diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e0cfbe6 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +# dotenv environment variables file +.env +.env* +**/.env +**/.env* + +# Sensitive Deploy Files +deploy/eb/ + +# tox +./.tox + + +# Backend +backend +!backend/schema.graphql \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..daf35a3 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,25 @@ +- Addresses XXX +- Depends on XXX + +## Changes + +- Detailed list or prose of changes +- Breaking changes +- Changes to configurations + +## This PR doesn't introduce any + +- [ ] temporary files, auto-generated files or secret keys +- [ ] build works +- [ ] eslint issues +- [ ] typescript issues +- [ ] codegen errors +- [ ] `console.log` meant for debugging +- [ ] typos +- [ ] unwanted comments +- [ ] conflict markers + +## This PR includes + +- [ ] Translation +- [ ] Permission \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..cba16c2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,66 @@ +name: CI + +on: + pull_request: + push: + branches: + - "main" + +env: + APP_GRAPHQL_CODEGEN_ENDPOINT: "./backend/schema.graphql" + APP_GRAPHQL_ENDPOINT: "http://web:8000/graphql/" + APP_TITLE: "ERCS" + APP_ENVIRONMENT: "development" + GITHUB_WORKFLOW: true + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + env: + PIPELINE_TYPE: ci + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install pnpm + run: npm install -g pnpm + + - uses: actions/setup-node@v4 + with: + node-version: "22.x" + cache: "pnpm" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Lint + run: pnpm lint + build: + name: Build + needs: [lint] + runs-on: ubuntu-latest + env: + PIPELINE_TYPE: ci + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - uses: pnpm/action-setup@v4 + name: Install pnpm + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: "22.x" + cache: "pnpm" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Generate Type + run: pnpm generate:type + + - name: Build + run: pnpm build \ No newline at end of file diff --git a/.github/workflows/publish-web-app-serve.yml b/.github/workflows/publish-web-app-serve.yml new file mode 100644 index 0000000..7b629dd --- /dev/null +++ b/.github/workflows/publish-web-app-serve.yml @@ -0,0 +1,24 @@ +name: Publish web app serve + +on: + workflow_dispatch: + push: + branches: + - develop + +permissions: + packages: write + +jobs: + publish_image: + name: Publish Docker Image + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: true + + - name: Publish web-app-serve + uses: toggle-corp/web-app-serve-action@v0.1.1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 82dced5..5b0cd42 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,9 @@ [submodule "backend"] path = backend url = https://github.com/toggle-corp/ercs-backend.git +[submodule "go-risk-module-api"] + path = go-risk-module-api + url = git@github.com:IFRCGo/go-risk-module-api.git +[submodule "go-api"] + path = go-api + url = https://github.com/IFRCGo/go-api.git diff --git a/Dockerfile b/Dockerfile index d343580..57de1cb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,41 +1,43 @@ FROM node:22-bookworm AS dev RUN apt-get update -y \ - && apt-get install -y --no-install-recommends \ - git bash g++ make \ - && rm -rf /var/lib/apt/lists/* - -RUN corepack enable + && apt-get install -y --no-install-recommends git \ + && rm -rf /var/lib/apt/lists/* \ + # NOTE: yarn > 1.22.19 breaks yarn-install invoked by pnpm + && npm install -g pnpm@10.33.0 yarn@1.22.19 --force \ + && git config --global --add safe.directory /code WORKDIR /code -RUN git config --global --add safe.directory /code - -COPY package.json pnpm-lock.yaml /code/ -RUN corepack prepare pnpm@latest --activate # -------------------------- Nginx - Builder -------------------------------- -FROM dev AS nginx-build +FROM dev AS web-app-serve-build + +COPY ./package.json ./pnpm-lock.yaml /code/ -RUN pnpm install --frozen-lockfile +RUN pnpm install -COPY . . +COPY . /code/ -ENV APP_TITLE=APP_TITLE_PLACEHOLDER -ENV APP_GRAPHQL_ENDPOINT=APP_GRAPHQL_ENDPOINT_PLACEHOLDER +# # Build variables (Requires backend pulled) +ENV APP_TITLE=ercs-eoc +ENV APP_ENVIRONMENT=development +ENV APP_GRAPHQL_ENDPOINT=http://localhost:8000 ENV APP_GRAPHQL_CODEGEN_ENDPOINT=./backend/schema.graphql +ENV APP_MAPBOX_TOKEN=APP_MAPBOX_TOKEN_PLACEHOLDER +ENV APP_GO_API=WEB_APP_SERVE_PLACEHOLDER__APP_GO_API_PLACEHOLDER +ENV APP_GO_URL=WEB_APP_SERVE_PLACEHOLDER__APP_GO_URL_PLACEHOLDER +ENV APP_GO_RISK_API_ENDPOINT=WEB_APP_SERVE_PLACEHOLDER__APP_GO_RISK_API_ENDPOINT_PLACEHOLDER -RUN pnpm generate:type && pnpm build + +RUN pnpm generate:type && WEB_APP_SERVE_ENABLED=true pnpm build # --------------------------------------------------------------------------- -FROM nginx:1 AS nginx-serve +FROM ghcr.io/toggle-corp/web-app-serve:v0.1.2 AS web-app-serve LABEL maintainer="Togglecorp Dev" LABEL org.opencontainers.image.source="https://github.com/ToogleCorp/ercs-client" -COPY ./nginx-serve/apply-config.sh /docker-entrypoint.d/ -COPY ./nginx-serve/nginx.conf.template /etc/nginx/templates/default.conf.template -COPY --from=nginx-build /code/build /code/build - +# Env for apply-config script ENV APPLY_CONFIG__SOURCE_DIRECTORY=/code/build/ -ENV APPLY_CONFIG__DESTINATION_DIRECTORY=/usr/share/nginx/html/ -ENV APPLY_CONFIG__OVERWRITE_DESTINATION=true \ No newline at end of file + +COPY --from=web-app-serve-build /code/build "$APPLY_CONFIG__SOURCE_DIRECTORY" \ No newline at end of file diff --git a/app/App.css b/app/App.css deleted file mode 100644 index e69de29..0000000 diff --git a/app/App.tsx b/app/App.tsx index dffadd1..6a065cc 100644 --- a/app/App.tsx +++ b/app/App.tsx @@ -1,70 +1,88 @@ import { createBrowserRouter, - RouterProvider -} from "react-router" + type RouteObject, + RouterProvider, +} from 'react-router'; +import mapboxgl from 'mapbox-gl'; -import type { RouteConfig } from "#root/config/routes.ts"; -import routes from "#root/config/routes.ts"; -import PageError from "#views/PageError/index.tsx"; +import { mapboxToken } from '#config'; +import type { RouteConfig } from '#root/config/routes.ts'; +import routes from '#root/config/routes.ts'; +import PageError from '#views/PageError/index.tsx'; const privateRoutes = Object.values(routes).filter( - ({ visibility }) => visibility === "is-authenticated", + ({ visibility }) => visibility === 'is-authenticated', ); const publicRoutes = Object.values(routes).filter( - ({ visibility }) => visibility === "is-anything", + ({ visibility }) => visibility === 'is-anything', ); const guestRoutes = Object.values(routes).filter( - ({ visibility }) => visibility === "is-not-authenticated", + ({ visibility }) => visibility === 'is-not-authenticated', ); -function mapRoute(routeConfig: RouteConfig) { +function mapRoute(routeConfig: RouteConfig): RouteObject { + // Only truly index routes: no path, index: true + if (routeConfig.index && !routeConfig.path) { + return { + index: true, + lazy: async () => { + const { default: Component } = await routeConfig.load(); + return { Component }; + }, + }; + } + return { - index: routeConfig.index, path: routeConfig.path, lazy: async () => { const { default: Component } = await routeConfig.load(); return { Component }; }, + children: routeConfig.children?.map(mapRoute), }; } +mapboxgl.accessToken = mapboxToken ?? ''; +mapboxgl.setRTLTextPlugin( + 'https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-rtl-text/v0.2.3/mapbox-gl-rtl-text.js', + // eslint-disable-next-line no-console + (err) => { console.error(err); }, + true, +); + const router = createBrowserRouter([ { errorElement: , lazy: async () => { - const { default: Component } = await import("./Root/index.tsx"); + const { default: Component } = await import('./Root/index.tsx'); return { Component }; }, children: [ { lazy: async () => { - const { default: Component } = - await import("./views/RootLayout/index.tsx"); + const { default: Component } = await import('./views/RootLayout/index.tsx'); return { Component }; }, children: [ { lazy: async () => { - const { default: Component } = - await import("./views/GuestLayout/index.tsx"); + const { default: Component } = await import('./views/GuestLayout/index.tsx'); return { Component }; }, children: guestRoutes.map(mapRoute), }, { lazy: async () => { - const { default: Component } = - await import("./views/PrivateLayout/index.tsx"); + const { default: Component } = await import('./views/PrivateLayout/index.tsx'); return { Component }; }, children: privateRoutes.map(mapRoute), }, { lazy: async () => { - const { default: Component } = - await import("./views/PublicLayout/index.tsx"); + const { default: Component } = await import('./views/PublicLayout/index.tsx'); return { Component }; }, children: publicRoutes.map(mapRoute), diff --git a/app/Root/config/routes.ts b/app/Root/config/routes.ts index fca3183..db2cb53 100644 --- a/app/Root/config/routes.ts +++ b/app/Root/config/routes.ts @@ -5,19 +5,163 @@ export interface RouteConfig { path?: string; load: () => Promise<{ default: () => React.JSX.Element | null }>; visibility: Visibility; + children?: RouteConfig[]; } const home: RouteConfig = { index: true, path: '/', load: () => import('#views/Home'), + visibility: 'is-anything', +}; + +const preparedness: RouteConfig = { + index: true, + path: '/preparedness', + load: () => import('#views/Preparedness'), + visibility: 'is-anything', + children: [ + { + path: 'emergency-alert', + load: () => import('#views/Preparedness/EmergencyAlert'), + visibility: 'is-anything', + }, + { + path: 'disaster-response', + load: () => import('#views/Preparedness/DisasterResponse'), + visibility: 'is-anything', + }, + { + path: 'pmer', + load: () => import('#views/Preparedness/DisasterResponse'), + visibility: 'is-anything', + }, + { + path: 'risk-analysis', + load: () => import('#views/Preparedness/RiskAnalysis'), + visibility: 'is-anything', + }, + ], +}; + +const dataAndReport: RouteConfig = { + index: true, + path: '/data-and-report', + load: () => import('#views/DataAndReport'), + visibility: 'is-anything', +}; + +const reportDetail: RouteConfig = { + index: true, + path: '/data-and-report/:id', + load: () => import('#views/DataAndReport/ReportDetail'), + visibility: 'is-anything', +}; +const capacityAndResources: RouteConfig = { + index: true, + path: '/capacity-and-resources', + load: () => import('#views/CapacityAndResources'), + visibility: 'is-anything', +}; +const capacityAndResourcesDetails: RouteConfig = { + index: true, + path: '/capacity-and-resources/:id', + load: () => import('#views/CapacityAndResources/CapacityAndResourcesDetails'), + visibility: 'is-anything', +}; + +const ourWork: RouteConfig = { + index: true, + path: '/our-work', + load: () => import('#views/OurWork'), + visibility: 'is-anything', + children: [ + { + path: 'emergency-response', + load: () => import('#views/OurWork/EmergencyResponse'), + visibility: 'is-anything', + }, + { + path: 'project-mapping', + load: () => import('#views/OurWork/ProjectMapping'), + visibility: 'is-anything', + }, + ], +}; +const galleries: RouteConfig = { + index: true, + path: '/galleries', + load: () => import('#views/Galleries'), visibility: 'is-authenticated', }; +// TODO: add terms and conditions and cookie policy page +const termsAndConditions: RouteConfig = { + index: true, + path: '/terms-and-conditions', + load: () => import('#views/Home'), + visibility: 'is-anything', +}; +const cookie: RouteConfig = { + index: true, + path: '/cookies-policy', + load: () => import('#views/Home'), + visibility: 'is-anything', +}; + +const team: RouteConfig = { + index: true, + path: '/team/:id', + load: () => import('#views/TeamList/Members'), + visibility: 'is-authenticated', +}; + +const teamList: RouteConfig = { + index: true, + path: '/teams', + load: () => import('#views/TeamList'), + visibility: 'is-authenticated', +}; + +const login: RouteConfig = { + index: true, + path: '/login', + load: () => import('#views/Login'), + visibility: 'is-not-authenticated', +}; + +function child(route: RouteConfig, path: string): RouteConfig { + const found = route.children?.find((c) => c.path === path); + if (!found) throw new Error(`Child route "${path}" not found in "${route.path}"`); + return { + ...found, + path: `${route.path}/${path}`, + }; +} + const routes = { home, -}; + ourWork, + preparedness, + dataAndReport, + galleries, + capacityAndResources, + termsAndConditions, + cookie, + team, + teamList, + login, + reportDetail, + capacityAndResourcesDetails, + // child routes + emergencyAlert: child(preparedness, 'emergency-alert'), + disasterResponse: child(preparedness, 'disaster-response'), + pmer: child(preparedness, 'pmer'), + riskAnalysis: child(preparedness, 'risk-analysis'), + emergencyResponse: child(ourWork, 'emergency-response'), + projectMapping: child(ourWork, 'project-mapping'), +} satisfies Record; export type RouteKeys = keyof typeof routes; diff --git a/app/Root/hooks/useRouteMatching.tsx b/app/Root/hooks/useRouteMatching.tsx new file mode 100644 index 0000000..a2b2a5a --- /dev/null +++ b/app/Root/hooks/useRouteMatching.tsx @@ -0,0 +1,39 @@ +import { use } from 'react'; +import { generatePath } from 'react-router'; + +import UserContext from '#contexts/UserContext'; +import type { RouteKeys } from '#root/config/routes'; +import routes from '#root/config/routes'; + +export interface Attrs { + [key: string]: string | undefined; +} + +function useRouteMatching(routeKey: RouteKeys, attrs?: Attrs) { + const { authenticated } = use(UserContext); + + const to = routes[routeKey]; + + if (!to) { + return undefined; + } + + const { + visibility, + path, + } = to; + + if (visibility === 'is-not-authenticated' && authenticated) { + return undefined; + } + + if (visibility === 'is-authenticated' && !authenticated) { + return undefined; + } + + return { + to: generatePath(path ?? '/', { ...attrs }), + }; +} + +export default useRouteMatching; diff --git a/app/Root/index.tsx b/app/Root/index.tsx index 2a0a687..ee570ab 100644 --- a/app/Root/index.tsx +++ b/app/Root/index.tsx @@ -1,8 +1,13 @@ -import { useState } from "react" +import { + Suspense, + useMemo, + useState, +} from 'react'; import { Cookies } from 'react-cookie'; import { Outlet } from 'react-router'; import { AlertContainer } from '@ifrc-go/ui'; import { AlertContext } from '@ifrc-go/ui/contexts'; +import { RequestContext } from '@togglecorp/toggle-request'; import { cacheExchange } from '@urql/exchange-graphcache'; import { Client, @@ -10,13 +15,24 @@ import { Provider as UrqlProvider, } from 'urql'; +import PreloadMessage from '#components/PreloadMessage'; +import { + api, + appTitle, + environment, +} from '#config'; import UserContext, { type UserContextInterface } from '#contexts/UserContext'; -import useAlertContextProviderValue from "#hooks/useAlertContextProviderValue"; - -import type { User } from './types/user'; +import type { MeQuery } from '#generated/types/graphql'; +import useAlertContextProviderValue from '#hooks/useAlertContextProviderValue'; +import { + processGoError, + processGoOptions, + processGoResponse, + processGoUrls, +} from '#utils/restRequest/go'; -const COOKIE_NAME = `ERCS-${import.meta.env.APP_ENVIRONMENT}-CSRFTOKEN`; -const GRAPHQL_ENDPOINT = `${import.meta.env.APP_GRAPHQL_ENDPOINT}/graphql/`; +const COOKIE_NAME = `ERCS-${environment}-CSRFTOKEN`; +const GRAPHQL_ENDPOINT = `${api}/graphql/`; const cookies = new Cookies(); const gqlClient = new Client({ @@ -27,7 +43,7 @@ const gqlClient = new Client({ ], fetchOptions: () => ({ headers: { - 'X-CSRFToken': cookies.get(COOKIE_NAME) || "taWf0Spres9M7HxChROyrQjTewfNgBds" , + 'X-CSRFToken': cookies.get(COOKIE_NAME), }, credentials: 'include', }), @@ -36,24 +52,43 @@ const gqlClient = new Client({ }); function Root() { - const [user, setUser] = useState(); + const [user, setUser] = useState(); const authenticated = !!user; - const userContext: UserContextInterface = { + const userContext: UserContextInterface = useMemo(() => ({ authenticated, user, setUser, + }), [authenticated, user]); - } + const requestContextValue = useMemo(() => ({ + transformUrl: processGoUrls, + transformOptions: processGoOptions, + transformResponse: processGoResponse, + transformError: processGoError, + }), []); const alertContextValue = useAlertContextProviderValue(); return ( - - - - - - + + + + + + {appTitle} + {' '} + loading... + + )} + > + + + + + + ); } diff --git a/app/components/BaseMap/index.tsx b/app/components/BaseMap/index.tsx new file mode 100644 index 0000000..ec4a793 --- /dev/null +++ b/app/components/BaseMap/index.tsx @@ -0,0 +1,121 @@ +import { + useContext, + useMemo, +} from 'react'; +import { LanguageContext } from '@ifrc-go/ui/contexts'; +import { ErrorBoundary } from '@sentry/react'; +import Map, { + MapLayer, + MapSource, +} from '@togglecorp/re-map'; +import { type SymbolLayer } from 'mapbox-gl'; + +import { + defaultMapOptions, + defaultMapStyle, + defaultNavControlOptions, + defaultNavControlPosition, +} from '#utils/map'; + +import styles from './styles.module.css'; + +type MapProps = Parameters[0]; + +type overrides = 'mapStyle' | 'mapOptions' | 'navControlShown' | 'navControlPosition' | 'navControlOptions' | 'scaleControlShown'; + +export type Props = Omit & { + baseLayers?: React.ReactNode; + withDisclaimer?: boolean; +} & Partial>; + +function BaseMap(props: Props) { + const { + baseLayers, + mapStyle, + mapOptions, + navControlShown, + navControlPosition, + navControlOptions, + scaleControlShown, + children, + ...otherProps + } = props; + + const { currentLanguage } = useContext(LanguageContext); + + const adminLabelLayerOptions : Omit = useMemo( + () => { + // ar, es, fr + let label: string; + if (currentLanguage === 'es') { + label = 'name_es'; + } else if (currentLanguage === 'ar') { + label = 'name_ar'; + } else if (currentLanguage === 'fr') { + label = 'name_fr'; + } else { + label = 'name'; + } + + return { + type: 'symbol', + layout: { + 'text-field': ['get', label], + }, + }; + }, + [currentLanguage], + ); + + return ( + + + + + + {baseLayers} + + {children} + + ); +} + +function BaseMapWithErrorBoundary(props: Props) { + return ( + + Failed to load map! + + )} + > + + + ); +} + +export default BaseMapWithErrorBoundary; diff --git a/app/components/BaseMap/styles.module.css b/app/components/BaseMap/styles.module.css new file mode 100644 index 0000000..4224164 --- /dev/null +++ b/app/components/BaseMap/styles.module.css @@ -0,0 +1,9 @@ +.map-error { + display: flex; + align-items: center; + justify-content: center; + background-color: var(--go-ui-color-background); + min-height: 20rem; + color: var(--go-ui-color-red); + font-size: var(--go-ui-font-size-lg); +} diff --git a/app/components/DisasterTypeSelectInput/index.tsx b/app/components/DisasterTypeSelectInput/index.tsx new file mode 100644 index 0000000..ec57e82 --- /dev/null +++ b/app/components/DisasterTypeSelectInput/index.tsx @@ -0,0 +1,69 @@ +import { use } from 'react'; +import { + SelectInput, + type SelectInputProps, +} from '@ifrc-go/ui'; + +import GoContext, { type DisasterTypes } from '#contexts/GoContext'; + +export type DisasterTypeItem = NonNullable[number]; + +function keySelector(type: DisasterTypeItem) { + return type.id; +} +function labelSelector(type: DisasterTypeItem) { + return type.name ?? '?'; +} + +type Props = SelectInputProps< + number, + NAME, + DisasterTypeItem, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + any, + 'value' | 'name' | 'options' | 'keySelector' | 'labelSelector' +> & { + className?: string; + name: NAME; + onChange: ( + newValue: number | undefined, + name: NAME, + option: DisasterTypeItem | undefined, + ) => void; + value: number | undefined | null; + optionsFilter?: (item: DisasterTypeItem) => boolean; + readOnly?: boolean; +} + +function DisasterTypeSelectInput(props: Props) { + const { + className, + name, + onChange, + value, + optionsFilter, + ...otherProps + } = props; + + const { disasterTypes } = use(GoContext); + + const options = optionsFilter + ? disasterTypes?.results.filter(optionsFilter) + : disasterTypes?.results; + + return ( + + ); +} + +export default DisasterTypeSelectInput; diff --git a/app/components/DropdownMenuItem/index.tsx b/app/components/DropdownMenuItem/index.tsx new file mode 100644 index 0000000..a17730a --- /dev/null +++ b/app/components/DropdownMenuItem/index.tsx @@ -0,0 +1,136 @@ +import { + useCallback, + useContext, +} from 'react'; +import { + Button, + type ButtonProps, + ConfirmButton, + type ConfirmButtonProps, +} from '@ifrc-go/ui'; +import { DropdownMenuContext } from '@ifrc-go/ui/contexts'; +import { isDefined } from '@togglecorp/fujs'; + +import Link, { type Props as LinkProps } from '#components/Link'; + +type CommonProp = { + persist?: boolean; + withoutFullWidth?: boolean; +} + +type ButtonTypeProps = Omit, 'type'> & { + type: 'button'; +} + +type LinkTypeProps = LinkProps & { + type: 'link'; + onClick?: never; +} + +type ConfirmButtonTypeProps = Omit, 'type'> & { + type: 'confirm-button', +} + +type Props = CommonProp & ( + ButtonTypeProps | LinkTypeProps | ConfirmButtonTypeProps +); + +function DropdownMenuItem(props: Props) { + const { + persist = false, + onClick, + withoutFullWidth, + ...remainingProps + } = props; + + const { setShowDropdown } = useContext(DropdownMenuContext); + + const handleLinkClick = useCallback( + () => { + if (!persist) { + setShowDropdown(false); + } + // TODO: maybe add onClick here? + }, + [setShowDropdown, persist], + ); + + const handleButtonClick = useCallback( + (name: NAME, e: React.MouseEvent) => { + if (remainingProps.type !== 'link') { + if (!persist) { + setShowDropdown(false); + } + + if (isDefined(onClick)) { + onClick(name, e); + } + } + }, + [setShowDropdown, persist, onClick, remainingProps.type], + ); + + if (remainingProps.type === 'link') { + const { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + type: _, + styleVariant = 'transparent', + colorVariant = 'text', + children, + ...otherProps + } = remainingProps; + + return ( + + {children} + + ); + } + + if (remainingProps.type === 'button') { + const { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + type: _, + styleVariant = 'transparent', + ...otherProps + } = remainingProps; + + return ( + + ); +} + +export default ExportButton; diff --git a/app/components/Footer/index.tsx b/app/components/Footer/index.tsx new file mode 100644 index 0000000..6085e0a --- /dev/null +++ b/app/components/Footer/index.tsx @@ -0,0 +1,226 @@ +import { + SocialFacebookIcon, + SocialInstagramIcon, + SocialLinkedinIcon, + SocialMediumIcon, + SocialTwitterIcon, + SocialYoutubeIcon, +} from '@ifrc-go/icons'; +import { + Container, + ListView, + PageContainer, +} from '@ifrc-go/ui'; +import { _cs } from '@togglecorp/fujs'; + +import Link from '#components/Link'; + +import styles from './styles.module.css'; + +const date = new Date(); +const year = date.getFullYear(); + +interface Props { + className?: string; +} + +function GlobalFooter(props: Props) { + const { + className, + } = props; + return ( + + + + +
+ ERCS EOC is a Ethiopian Red Cross platform to connect + information on emergency + needs with the right response. +
+
+ © ERCS EOC + {' '} + {year} +
+
+
+ + + + ifrc.org + + + rcrcsims.org + + + data.ifrc.org + + + + + + + Cookie Policy + + + Terms and Conditions + + + + + + + Dataset + + + Project Mapping + + + Online Interactive + + + + + + + ercsinfo@redcrosseth.org + + + + {/* TODO: change twitter icon in ifrc go icon to X */} + + + + + + + {/* TODO: add flickr icon in ifrc go icon */} + + + + + + + + + + + + + + + + + +
+
+ ); +} + +export default GlobalFooter; diff --git a/app/components/Footer/styles.module.css b/app/components/Footer/styles.module.css new file mode 100644 index 0000000..6e20f6a --- /dev/null +++ b/app/components/Footer/styles.module.css @@ -0,0 +1,11 @@ +.footer { + background-color: var(--go-ui-color-primary-gray); + + * { + color: var(--go-ui-color-text-on-dark); + } + + .social-icon { + font-size: var(--go-ui-height-social-icon); + } +} \ No newline at end of file diff --git a/app/components/GlobalMap/index.tsx b/app/components/GlobalMap/index.tsx new file mode 100644 index 0000000..39de995 --- /dev/null +++ b/app/components/GlobalMap/index.tsx @@ -0,0 +1,177 @@ +import { + useMemo, + useState, +} from 'react'; +import { MapLayer } from '@togglecorp/re-map'; +import { + type Expression, + type FillLayer, + type FillPaint, + type LngLatLike, + type MapboxGeoJSONFeature, +} from 'mapbox-gl'; + +import BaseMap, { type Props as BaseMapProps } from '#components/BaseMap'; +import { COLOR_BLACK } from '#utils/constants'; + +export interface AdminZeroFeatureProperties { + country_id: number; + disputed: boolean; + independent: boolean; + is_deprecated: boolean; + name: string; + name_ar: string; + name_es: string; + name_fr: string; + record_type: string; + + // NOTE: we check for undefined iso3 before triggering + // onClick and onHover + iso3: string; + + fdrs?: string; + iso?: string; + region_id?: number; +} + +const KOSOVO_ISO3 = 'XKX'; +const WESTERN_SAHARA_ISO3 = 'ESH'; + +const overlappedDisputedCountriesIso3 = [ + KOSOVO_ISO3, + WESTERN_SAHARA_ISO3, +]; + +const adminZeroHighlightPaint: FillPaint = { + 'fill-color': COLOR_BLACK, + 'fill-opacity': [ + 'case', + ['all', + ['==', ['feature-state', 'hovered'], true], + ['!=', ['get', 'iso3'], null], + ], + 0.2, + 0, + ], +}; + +interface Props extends BaseMapProps { + adminZeroFillPaint?: mapboxgl.FillPaint, + onAdminZeroFillHover?: ( + hoveredFeatureProperties: AdminZeroFeatureProperties | undefined + ) => void; + onAdminZeroFillClick?: ( + clickedFeatureProperties: AdminZeroFeatureProperties, + lngLat: LngLatLike, + ) => void; +} + +function GlobalMap(props: Props) { + const { + onAdminZeroFillHover: onHover, + onAdminZeroFillClick: onClick, + adminZeroFillPaint, + baseLayers, + ...baseMapProps + } = props; + + const [hoveredCountryIso3, setHoveredCountryIso3] = useState(); + + const handleFeatureMouseEnter = (feature: MapboxGeoJSONFeature) => { + const hoveredFeatureProperties = feature.properties as ( + AdminZeroFeatureProperties | undefined + ); + + setHoveredCountryIso3(hoveredFeatureProperties?.iso3); + + if (onHover) { + onHover(hoveredFeatureProperties); + } + }; + + const handleFeatureMouseLeave = () => { + setHoveredCountryIso3(undefined); + + if (onHover) { + onHover(undefined); + } + }; + + const handleClick = (feature: MapboxGeoJSONFeature, lngLat: LngLatLike) => { + if (onClick) { + onClick( + feature.properties as AdminZeroFeatureProperties, + lngLat, + ); + } + + return true; + }; + + const fillSortKey = useMemo(() => [ + 'match', + ['get', 'iso3'], + // NOTE: Hovered geoarea should be at the top + hoveredCountryIso3 ?? '???', + 2, + // NOTE: After that, we should have geoarea that is 100% + // included in another geoarea + ...(overlappedDisputedCountriesIso3.filter( + (iso3) => !(iso3 === hoveredCountryIso3), + ).flatMap((iso3) => [iso3, 1])), + // NOTE: Everything else should be after that + 0, + ], [hoveredCountryIso3]); + + const adminZeroHighlightLayerOptions = useMemo>( + () => ({ + type: 'fill', + layout: { + visibility: 'visible', + 'fill-sort-key': fillSortKey, + }, + paint: adminZeroHighlightPaint, + filter: ['!=', ['get', 'iso3'], null], + }), + [fillSortKey], + ); + + const adminZeroBaseLayerOptions = useMemo>( + () => ({ + type: 'fill', + layout: { + visibility: 'visible', + 'fill-sort-key': fillSortKey, + }, + paint: adminZeroFillPaint, + }), + [fillSortKey, adminZeroFillPaint], + ); + + return ( + + + {(onHover || onClick) && ( + + )} + {baseLayers} + + )} + /> + ); +} + +export default GlobalMap; diff --git a/app/components/GoMapContainer/index.tsx b/app/components/GoMapContainer/index.tsx new file mode 100644 index 0000000..c39268f --- /dev/null +++ b/app/components/GoMapContainer/index.tsx @@ -0,0 +1,345 @@ +import { + useCallback, + useEffect, + useRef, +} from 'react'; +import { + ArtboardLineIcon, + CloseFillIcon, + CloseLineIcon, + DownloadTwoLineIcon, +} from '@ifrc-go/icons'; +import { + Button, + Container, + DateOutput, + IconButton, + InfoPopup, + Label, + ListView, + RawButton, +} from '@ifrc-go/ui'; +import { useBooleanState } from '@ifrc-go/ui/hooks'; +import { resolveToComponent } from '@ifrc-go/ui/utils'; +import { + _cs, + isDefined, +} from '@togglecorp/fujs'; +import { MapContainer } from '@togglecorp/re-map'; + +import Link from '#components/Link'; +import goLogo from '#resources/image/logo.png'; + +// import FileSaver from 'file-saver'; +// import { toPng } from 'html-to-image'; +import styles from './styles.module.css'; + +interface Props { + className?: string; + title: string; + footer?: React.ReactNode; + withoutDownloadButton?: boolean; + withPresentationMode?: boolean; + presentationModeAdditionalBeforeContent?: React.ReactNode; + presentationModeAdditionalAfterContent?: React.ReactNode; + onPresentationModeChange?: (newPresentationMode: boolean) => void; + children?: React.ReactNode; +} + +function GoMapContainer(props: Props) { + const mbToken = import.meta.env.APP_MAPBOX_TOKEN; + const { + className, + title = 'IFRC GO - Map', + footer, + withoutDownloadButton = false, + withPresentationMode = false, + presentationModeAdditionalBeforeContent, + presentationModeAdditionalAfterContent, + onPresentationModeChange, + children, + } = props; + + const mapSources = resolveToComponent( + 'Sources: ICRC,', + { + uncodsLink: ( + + UNCODs + + ), + }, + ); + + const [ + printMode, + { + setTrue: enterPrintMode, + setFalse: exitPrintMode, + }, + ] = useBooleanState(false); + + const [ + presentationMode, + { + setTrue: setPresentationModeTrue, + setFalse: setPresentationModeFalse, + }, + ] = useBooleanState(false); + + const containerRef = useRef(null) as React.RefObject; + + const enterPresentationMode = useCallback(() => { + if (isDefined(containerRef.current)) { + containerRef.current.requestFullscreen(); + } + }, []); + + const exitPresentationMode = useCallback(() => { + if (isDefined(document.fullscreenElement)) { + document.exitFullscreen(); + } + }, []); + + const handleFullScreenChange = useCallback(() => { + if (isDefined(document.fullscreenElement)) { + setPresentationModeTrue(); + } else { + setPresentationModeFalse(); + } + }, [setPresentationModeTrue, setPresentationModeFalse]); + + useEffect(() => { + document.addEventListener('fullscreenchange', handleFullScreenChange); + + return (() => { + document.removeEventListener('fullscreenchange', handleFullScreenChange); + }); + }, [handleFullScreenChange]); + + useEffect(() => { + if (isDefined(onPresentationModeChange)) { + onPresentationModeChange(presentationMode); + } + }, [presentationMode, onPresentationModeChange]); + + // const alert = useAlert(); + // const handleDownloadClick = useCallback(() => { + // if (!containerRef?.current) { + // alert.show( + // "Failed to download map. Try again.", + // { variant: 'danger' }, + // ); + // exitPrintMode(); + // return; + // } + // toPng(containerRef.current, { skipAutoScale: false }) + // .then((data) => FileSaver.saveAs(data, title)) + // .finally(exitPrintMode); + // }, [exitPrintMode, title, alert]); + + return ( + + {title} + + + )} + headerActions={( + <> + {printMode && ( + + + + + + + + + )} + {presentationMode && ( + + + + )} + {printMode && ( + + IFRC GO logo + + )} + + )} + spacing={presentationMode ? 'xl' : 'none'} + withPadding={presentationMode} + > + + {presentationMode && presentationModeAdditionalBeforeContent} +
+ + + + + {mapSources} + + + + © Mapbox + + + © OpenStreetMap + + + Improve this map + + + + )} + /> + {withPresentationMode && !printMode && !presentationMode && ( + + )} + {!printMode && !presentationMode && !withoutDownloadButton && ( + + + + )} +
+ {children} +
+
+ {footer && ( + + {footer} + + )} + {presentationMode && presentationModeAdditionalAfterContent} +
+
+ ); +} + +export default GoMapContainer; diff --git a/app/components/GoMapContainer/styles.module.css b/app/components/GoMapContainer/styles.module.css new file mode 100644 index 0000000..c3fc40c --- /dev/null +++ b/app/components/GoMapContainer/styles.module.css @@ -0,0 +1,76 @@ +.go-map-container { + .go-icon { + height: var(--go-ui-height-compact-status-icon); + } + + .relative-wrapper { + position: relative; + isolation: isolate; + + .map { + height: var(--go-ui-height-map-md); + } + + .download-button { + position: absolute; + top: 5rem; + + /* NOTE: Exactly as mapbox */ + right: 10px; + border: var(--go-ui-width-separator-md) solid var(--go-ui-color-separator); + border-radius: var(--go-ui-border-radius-md); + background-color: var(--go-ui-color-foreground); + padding: 0 var(--go-ui-spacing-2xs); + font-size: var(--go-ui-height-icon-multiplier); + } + + .map-disclaimer { + position: absolute; + bottom: var(--go-ui-spacing-xs); + left: calc(var(--mapbox-icon-width) + var(--go-ui-spacing-sm)); + background-color: var(--go-ui-color-white); + padding: 0 var(--go-ui-spacing-2xs); + font-size: var(--go-ui-font-size-sm); + } + + .presentation-mode-button { + position: absolute; + top: var(--go-ui-spacing-sm); + left: var(--go-ui-spacing-sm); + } + + .content { + position: absolute; + bottom: calc(1.5rem + 2 * var(--go-ui-spacing-xs)); + left: var(--go-ui-spacing-sm); + } + } + + &.print-mode { + position: relative; + border: var(--go-ui-width-separator-thin) solid var(--go-ui-color-separator); + + .relative-wrapper { + border-top: var(--go-ui-width-separator-lg) solid var(--go-ui-color-primary-red); + + .map { + height: calc(var(--go-ui-width-separator-lg) + var(--go-ui-height-map-md)); + } + } + + .floating-actions { + position: absolute; + top: 0; + right: 0; + transform: translateY(-100%); + background-color: var(--go-ui-color-foreground); + } + } + + &.presentation-mode { + background-color: var(--go-ui-color-background); + width: 100vw; + height: 100vh; + overflow: auto; + } +} diff --git a/app/components/ImminentEventListItem/index.tsx b/app/components/ImminentEventListItem/index.tsx new file mode 100644 index 0000000..8a0e5d1 --- /dev/null +++ b/app/components/ImminentEventListItem/index.tsx @@ -0,0 +1,84 @@ +import { + useEffect, + useRef, +} from 'react'; +import { + ChevronDownLineIcon, + ChevronUpLineIcon, +} from '@ifrc-go/icons'; +import { + Button, + Container, +} from '@ifrc-go/ui'; + +interface Props { + className?: string; + eventId: number | string; + heading: React.ReactNode; + description: React.ReactNode; + expanded: boolean; + onExpandClick: (eventId: number | string) => void; + children?: React.ReactNode; +} + +function ImminentEventListItem(props: Props) { + const { + eventId, + className, + heading, + description, + expanded, + onExpandClick, + children, + } = props; + + const elementRef = useRef(null); + useEffect( + () => { + if (expanded && elementRef.current) { + const y = window.scrollY; + const x = window.scrollX; + elementRef.current.scrollIntoView({ + behavior: 'instant', + block: 'start', + }); + // NOTE: We need to scroll back because scrollIntoView also + // scrolls the parent container + window.scroll(x, y); + } + }, + [expanded], + ); + + return ( + } + className={className} + heading={heading ?? '--'} + headingLevel={5} + headerActions={( + + )} + headerDescription={description} + spacing="sm" + withDarkBackground + withPadding + withoutSpacingOpticalCorrection + withContentWell + > + {children} + + ); +} + +export default ImminentEventListItem; diff --git a/app/components/InfoCard/index.tsx b/app/components/InfoCard/index.tsx new file mode 100644 index 0000000..f5f1118 --- /dev/null +++ b/app/components/InfoCard/index.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { + Container, + type ContainerProps, + Description, + Heading, + InlineLayout, + ListView, +} from '@ifrc-go/ui'; + +import styles from './styles.module.css'; + +interface InfoCardProps extends Omit { + icon: React.ReactNode; + title: string; + description: string; +} + +function InfoCard({ + icon, + title, + description, + withDarkBackground = true, + ...containerProps +}: InfoCardProps) { + return ( + + + + {icon} + + )} + spacing="2xs" + > + + {title} + + + + {description} + + + + ); +} + +export default InfoCard; diff --git a/app/components/InfoCard/styles.module.css b/app/components/InfoCard/styles.module.css new file mode 100644 index 0000000..33dab08 --- /dev/null +++ b/app/components/InfoCard/styles.module.css @@ -0,0 +1,9 @@ +.icon { + line-height: 1; + color: var(--go-ui-color-primary-red); +} + +.info-card { + border-bottom: 1px solid var(--go-ui-color-gray-30) ; + border-radius: 0 !important; +} \ No newline at end of file diff --git a/app/components/KeyCard/index.tsx b/app/components/KeyCard/index.tsx new file mode 100644 index 0000000..b2afe53 --- /dev/null +++ b/app/components/KeyCard/index.tsx @@ -0,0 +1,119 @@ +import React from 'react'; +import { + Button, + Container, + Description, + InlineLayout, + KeyFigure, + type KeyFigureProps, + ListView, +} from '@ifrc-go/ui'; +import { + _cs, + isDefined, +} from '@togglecorp/fujs'; + +import styles from './styles.module.css'; + +type KeyCardProps = KeyFigureProps & { + withShadow?: boolean + icon?: React.ReactNode; + pillText?: React.ReactNode; + info?: string; + withIconBackground?: boolean; + viewButton? :boolean; + onViewClick?: () => void +} + +function KeyCard(props : KeyCardProps) { + const { + className, + icon, + pillText, + info, + withShadow, + withIconBackground, + viewButton, + onViewClick, + ...keyFigureProps + } = props; + + return ( + + + {(isDefined(icon) || isDefined(pillText)) && ( + + {icon && ( + + {icon} + + )} + {pillText && ( + + + {pillText} + + + )} + + + )} + + + + {isDefined(icon) && ( + + {info} + + )} + + {isDefined(viewButton) + && ( + + View + + )} + /> + )} + + + + ); +} + +export default KeyCard; diff --git a/app/components/KeyCard/styles.module.css b/app/components/KeyCard/styles.module.css new file mode 100644 index 0000000..12db0bf --- /dev/null +++ b/app/components/KeyCard/styles.module.css @@ -0,0 +1,21 @@ +.key-card { + .icon { + line-height: 1; + color: var(--go-ui-color-primary-red); + font-size: var(--go-ui-height-key-figure-icon); + } + + .icon-with-background { + border-radius: var(--go-ui-spacing-sm); + background-color: var(--go-ui-color-red-10); + padding: var(--go-ui-spacing-sm);; + width: var(--go-ui-spacing-3xl); + height: var(--go-ui-spacing-3xl); + } + + .pill { + border-radius: var(--go-ui-spacing-md); + padding: var(--go-ui-spacing-3xs) var(--go-ui-spacing-sm) ; + font-weight: 500; + } +} diff --git a/app/components/Link/index.tsx b/app/components/Link/index.tsx new file mode 100644 index 0000000..eedc34a --- /dev/null +++ b/app/components/Link/index.tsx @@ -0,0 +1,144 @@ +import { + Link as RouterLink, + type LinkProps, +} from 'react-router'; +import { + ArrowRightUpLineIcon, + ChevronRightLineIcon, +} from '@ifrc-go/icons'; +import { + ButtonLayout, + type ButtonLayoutProps, +} from '@ifrc-go/ui'; +import { _cs } from '@togglecorp/fujs'; + +import type { RouteKeys } from '#root/config/routes'; +import useRouteMatching, { type Attrs } from '#root/hooks/useRouteMatching'; + +import styles from './styles.module.css'; + +interface InternalLinkProps extends Omit { + external?: never; + href?: never; + to: RouteKeys; + attrs?: Attrs +} + +interface ExternalLinkProps extends Omit, 'href'> { + external: true; + href: string | undefined | null; + to?: never; + attrs?: never +} + +export type CommonLinkProps = ButtonLayoutProps & { + withLinkIcon?: boolean + withUnderline?: boolean +}; + +export type Props = CommonLinkProps & (InternalLinkProps | ExternalLinkProps); + +function Link(props: Props) { + const { + to, + attrs, + className, + before, + children, + after, + childrenContainerClassName, + colorVariant = 'text', + styleVariant = 'action', + withoutPadding, + spacing, + external, + href, + withEllipsizedContent, + withFullWidth, + disabled, + textSize, + withLinkIcon, + withUnderline, + spacingOffset = styleVariant === 'action' ? -5 : -3, + ...otherProps + } = props; + + const routeData = useRouteMatching(to as RouteKeys, attrs); + const content = ( + + {after} + {withLinkIcon && external && ( + + )} + {withLinkIcon && !external && ( + + )} + + )} + > + {children} + + ); + + if (external) { + if (!href) { + return ( + + {content} + + ); + } + return ( + + {content} + + ); + } + + if (!routeData) { + return null; + } + + return ( + + {content} + + ); +} + +export default Link; diff --git a/app/components/Link/styles.module.css b/app/components/Link/styles.module.css new file mode 100644 index 0000000..4c5691c --- /dev/null +++ b/app/components/Link/styles.module.css @@ -0,0 +1,17 @@ +.link { + display: contents; + + .layout { + &.with-underline { + text-decoration: underline; + } + } + + .link-icon { + font-size: var(--go-ui-height-icon-multiplier); + } + + .children-container { + display: inline-flex; + } +} \ No newline at end of file diff --git a/app/components/MapPopup/index.tsx b/app/components/MapPopup/index.tsx new file mode 100644 index 0000000..e247b8a --- /dev/null +++ b/app/components/MapPopup/index.tsx @@ -0,0 +1,76 @@ +import { useMemo } from 'react'; +import { CloseLineIcon } from '@ifrc-go/icons'; +import { + Button, + Container, + type ContainerProps, +} from '@ifrc-go/ui'; +import { _cs } from '@togglecorp/fujs'; +import { MapPopup as BasicMapPopup } from '@togglecorp/re-map'; + +import styles from './styles.module.css'; + +interface Props extends ContainerProps { + coordinates: mapboxgl.LngLatLike; + children: React.ReactNode; + onCloseButtonClick: () => void; + popupClassName?: string; +} + +function MapPopup(props: Props) { + const { + children, + coordinates, + onCloseButtonClick, + headerActions, + popupClassName, + ...containerProps + } = props; + + const popupOptions = useMemo(() => ({ + closeButton: false, + closeOnClick: false, + closeOnMove: false, + offset: 8, + className: _cs(styles.mapPopup, popupClassName), + maxWidth: 'unset', + }), [popupClassName]); + + return ( + + ); +} + +export default MapPopup; diff --git a/app/components/MapPopup/styles.module.css b/app/components/MapPopup/styles.module.css new file mode 100644 index 0000000..f4918d0 --- /dev/null +++ b/app/components/MapPopup/styles.module.css @@ -0,0 +1,46 @@ +.map-popup { + display: flex; + padding: 0; + width: 20rem; + min-height: 10rem; + max-height: 20rem; + overflow: auto; + font-family: var(--go-ui-font-family-sans-serif); + font-size: var(--go-ui-font-size-md); + + & :global(.mapboxgl-tip) { + flex-shrink: 0; + } + + & :global(.mapboxgl-popup-content) { + display: flex; + flex-direction: column; + flex-grow: 1; + padding: 0; + overflow: auto; + } + + & :global(.mapboxgl-popup-content > div) { + display: flex; + flex-direction: column; + flex-grow: 1; + padding: 0; + overflow: auto; + } + + .container { + flex-grow: 1; + border: var(--go-ui-width-separator-thin) solid var(--go-ui-color-separator); + width: 100%; + height: 100%; + overflow: auto; + + .close-button { + font-size: var(--go-ui-height-icon-multiplier); + } + + .content { + overflow: auto; + } + } +} diff --git a/app/components/NavLink/index.tsx b/app/components/NavLink/index.tsx new file mode 100644 index 0000000..fab8d86 --- /dev/null +++ b/app/components/NavLink/index.tsx @@ -0,0 +1,87 @@ +import { + NavLink as RouterNavLink, + type NavLinkProps, + useNavigate, +} from 'react-router'; +import { + ButtonLayout, + type ButtonLayoutProps, +} from '@ifrc-go/ui'; +import { _cs } from '@togglecorp/fujs'; + +import type { RouteKeys } from '#root/config/routes'; +import useRouteMatching, { type Attrs } from '#root/hooks/useRouteMatching'; + +import styles from './styles.module.css'; + +export type Props = Omit & ButtonLayoutProps & { + to: RouteKeys; + navigateTo?: RouteKeys; + attrs?: Attrs; + activeClassName?: string; +}; + +function NavLink(props: Props) { + const { + to, + navigateTo, + attrs, + className, + before, + children, + after, + childrenContainerClassName, + colorVariant = 'text', + styleVariant = 'action', + withoutPadding, + spacing, + activeClassName, + onClick, + ...otherProps + } = props; + const navigate = useNavigate(); + const routeData = useRouteMatching(to, attrs); + const navigateRouteData = useRouteMatching(navigateTo ?? to, attrs); + + if (!routeData || !navigateRouteData) return null; + + const handleClick = (e: React.MouseEvent) => { + if (navigateTo) { + e.preventDefault(); + navigate(navigateRouteData.to); + } + onClick?.(e); + }; + + return ( + _cs( + styles.smartNavLink, + isActive && styles.active, + isActive && activeClassName, + )} + > + + {children} + + + ); +} + +export default NavLink; diff --git a/app/components/NavLink/styles.module.css b/app/components/NavLink/styles.module.css new file mode 100644 index 0000000..3bb32ba --- /dev/null +++ b/app/components/NavLink/styles.module.css @@ -0,0 +1,11 @@ +.smart-nav-link { + display: contents; + + &.active { + .button-layout { + opacity: 1; + color: var(--go-ui-color-red ); + } + } + +} diff --git a/app/components/Navbar/index.tsx b/app/components/Navbar/index.tsx new file mode 100644 index 0000000..7527077 --- /dev/null +++ b/app/components/Navbar/index.tsx @@ -0,0 +1,127 @@ +import { use } from 'react'; +import { useNavigate } from 'react-router'; +import { + Button, + Heading, + Image, + ListView, + NavigationTabList, + PageContainer, +} from '@ifrc-go/ui'; + +import Link from '#components/Link'; +import NavLink from '#components/NavLink'; +import UserContext from '#contexts/UserContext'; +import Logo from '#resources/image/logo.png'; + +import styles from './styles.module.css'; + +function Navbar() { + const { authenticated } = use(UserContext); + const navigate = useNavigate(); + + const handleLogin = () => { + navigate('/login'); + }; + return ( + + ); +} + +export default Navbar; diff --git a/app/components/Navbar/styles.module.css b/app/components/Navbar/styles.module.css new file mode 100644 index 0000000..415ee55 --- /dev/null +++ b/app/components/Navbar/styles.module.css @@ -0,0 +1,22 @@ +.navbar { + border-bottom: var(--go-ui-width-separator-thin) solid var(--go-ui-color-separator); + background-color: var(--go-ui-color-white); + + .top { + border-bottom: var(--go-ui-width-separator-md) solid var(--go-ui-color-primary-red); + + .top-content { + padding: var(--go-ui-spacing-sm) var(--go-ui-spacing-lg); + + .icon { + width: var(--go-ui-height-brand-icon); + height: var(--go-ui-height-brand-icon); + } + } + } + + .bottom { + padding: var(--go-ui-spacing-sm) var(--go-ui-spacing-lg); + } +} + diff --git a/app/components/NavigationTab/index.tsx b/app/components/NavigationTab/index.tsx new file mode 100644 index 0000000..3759de1 --- /dev/null +++ b/app/components/NavigationTab/index.tsx @@ -0,0 +1,150 @@ +import { useContext } from 'react'; +import { + Link as RouterLink, + type LinkProps, + useMatch, +} from 'react-router'; +import { + ArrowRightUpLineIcon, + ChevronRightLineIcon, +} from '@ifrc-go/icons'; +import { + TabLayout, + type TabLayoutProps, +} from '@ifrc-go/ui'; +import { NavigationTabContext } from '@ifrc-go/ui/contexts'; + +import type { RouteKeys } from '#root/config/routes'; +import useRouteMatching, { type Attrs } from '#root/hooks/useRouteMatching'; + +import styles from './styles.module.css'; + +interface InternalLinkProps extends Omit { + external?: never; + href?: never; + to: RouteKeys; + attrs?: Attrs +} + +interface ExternalLinkProps extends Omit, 'href'> { + external: true; + href: string | undefined | null; + to?: never; + attrs?: never +} + +type CommonProps = Omit & { + withEllipsizedContent?: boolean; + withLinkIcon?: boolean; + withUnderline?: boolean; +} + +export type Props = CommonProps & (InternalLinkProps | ExternalLinkProps); + +function NavigationTab(props: Props) { + const { + to, + attrs, + className, + before, + children, + after, + childrenContainerClassName, + withoutPadding, + spacing, + external, + href, + withEllipsizedContent, + spacingOffset, + disabled, + withLinkIcon, + tabWrapperClassName, + errored, + stepCompleted, + isFirstStep, + isLastStep, + ...otherProps + } = props; + + const { + colorVariant, + styleVariant, + } = useContext(NavigationTabContext); + const routeData = useRouteMatching(to as RouteKeys, attrs); + + const match = useMatch(routeData?.to ?? ''); + const isActive = !!match; + + const content = ( + + {after} + {withLinkIcon && external && ( + + )} + {withLinkIcon && !external && ( + + )} + + )} + > + {children} + + ); + + if (external) { + if (!href) { + return ( + + {content} + + ); + } + return ( + + {content} + + ); + } + + if (!routeData) { + return null; + } + + return ( + + {content} + + ); +} + +export default NavigationTab; diff --git a/app/components/NavigationTab/styles.module.css b/app/components/NavigationTab/styles.module.css new file mode 100644 index 0000000..3e31c29 --- /dev/null +++ b/app/components/NavigationTab/styles.module.css @@ -0,0 +1,3 @@ +.navigation-tab { + display: contents; +} diff --git a/app/components/Page/index.tsx b/app/components/Page/index.tsx new file mode 100644 index 0000000..7877ae3 --- /dev/null +++ b/app/components/Page/index.tsx @@ -0,0 +1,119 @@ +import { + type RefObject, + useEffect, +} from 'react'; +import { + ListView, + PageContainer, + PageHeader, +} from '@ifrc-go/ui'; +import { + _cs, + isDefined, + isNotDefined, +} from '@togglecorp/fujs'; + +import styles from './styles.module.css'; + +interface Props { + className?: string; + title?: string; + actions?: React.ReactNode; + heading?: React.ReactNode; + description?: React.ReactNode; + mainSectionContainerClassName?: string; + breadCrumbs?: React.ReactNode; + info?: React.ReactNode; + children?: React.ReactNode; + mainSectionClassName?: string; + wikiLink?: React.ReactNode; + withBackgroundColorInMainSection?: boolean; + elementRef?: RefObject; + blockingContent?: React.ReactNode; + beforeHeaderContent?: React.ReactNode; +} + +function Page(props: Props) { + const { + className, + title, + actions, + heading, + description, + breadCrumbs, + info, + children, + mainSectionContainerClassName, + mainSectionClassName, + wikiLink, + withBackgroundColorInMainSection, + elementRef, + blockingContent, + beforeHeaderContent, + } = props; + + useEffect(() => { + if (isDefined(title)) { + document.title = title; + } + }, [title]); + + const showPageContainer = !!breadCrumbs + || !!heading + || !!description + || !!info + || !!actions + || !!wikiLink; + + return ( +
+ {beforeHeaderContent && ( + + {beforeHeaderContent} + + )} + {isNotDefined(blockingContent) && showPageContainer && ( + + )} + {isNotDefined(blockingContent) && ( + + + { children } + + + )} +
+ ); +} + +export default Page; diff --git a/app/components/Page/styles.module.css b/app/components/Page/styles.module.css new file mode 100644 index 0000000..bfd24da --- /dev/null +++ b/app/components/Page/styles.module.css @@ -0,0 +1,29 @@ +.page { + display: flex; + flex-direction: column; + flex-grow: 1; + + .machine-translation-warning { + background-color: var(--go-ui-color-warning); + padding: var(--go-ui-spacing-sm); + text-align: center; + color: var(--go-ui-color-white); + } + + .page-header { + background-color: var(--go-ui-color-background); + } + + .main-section-container { + flex-grow: 1; + background-color: var(--go-ui-color-white); + + &.with-background-color { + background-color: var(--go-ui-color-background); + } + + .main-section { + padding: var(--go-ui-spacing-4xl) var(--go-ui-spacing-lg); + } + } +} \ No newline at end of file diff --git a/app/components/PdfViewer/index.tsx b/app/components/PdfViewer/index.tsx new file mode 100644 index 0000000..bcdf7c4 --- /dev/null +++ b/app/components/PdfViewer/index.tsx @@ -0,0 +1,75 @@ +import 'react-pdf/dist/Page/AnnotationLayer.css'; +import 'react-pdf/dist/Page/TextLayer.css'; + +import { + useCallback, + useState, +} from 'react'; +import { + Document, + Page as PdfPage, + pdfjs, +} from 'react-pdf'; + +pdfjs.GlobalWorkerOptions.workerSrc = `https://unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`; + +interface PdfViewerProps { + file: string; + loadingMessage?: React.ReactNode; + errorMessage?: React.ReactNode; +} + +function PdfViewer({ + file, + loadingMessage =

Loading PDF…

, + errorMessage =

Failed to load PDF.

, +}: PdfViewerProps) { + const [numPages, setNumPages] = useState(0); + const [loadedPages, setLoadedPages] = useState(0); + const [containerWidth, setContainerWidth] = useState(); + + const allPagesLoaded = numPages > 0 && loadedPages === numPages; + const onContainerRef = useCallback((node: HTMLDivElement | null): void => { + if (node) { + setContainerWidth(node.getBoundingClientRect().width); + } + }, []); + + const onDocumentLoadSuccess = useCallback( + ({ numPages: nextNumPages }: { numPages: number }): void => { + setNumPages(nextNumPages); + }, + [], + ); + + const onPageLoadSuccess = useCallback((): void => { + setLoadedPages((prev) => prev + 1); + }, []); + return ( +
+ {!allPagesLoaded && loadingMessage} +
+ + {Array.from({ length: numPages }, (_, index) => ( + + ))} + +
+
+ ); +} + +export default PdfViewer; diff --git a/app/components/PowerBiEmbed/index.tsx b/app/components/PowerBiEmbed/index.tsx new file mode 100644 index 0000000..d8c0877 --- /dev/null +++ b/app/components/PowerBiEmbed/index.tsx @@ -0,0 +1,43 @@ +/* eslint-disable no-console */ +import type { IReportEmbedConfiguration } from 'powerbi-client'; +import { models } from 'powerbi-client'; +import { PowerBIEmbed as PowerBI } from 'powerbi-client-react'; +import type { ICustomEvent } from 'service'; + +import styles from './styles.module.css'; + +function PowerBIEmbed({ embedUrl }: { embedUrl: string }) { + const embedConfig: IReportEmbedConfiguration = { + type: 'report', + embedUrl, + id: undefined, + accessToken: undefined, + tokenType: models.TokenType.Embed, + settings: { + panes: { + filters: { visible: false }, + }, + layoutType: models.LayoutType.Custom, + customLayout: { + displayOption: models.DisplayOption.FitToWidth, + }, + navContentPaneEnabled: true, + }, + }; + + return ( + console.log('Report loaded')], + ['rendered', () => console.log('Report rendered')], + ['error', (event?: ICustomEvent) => console.error('Error:', event?.detail)], + ]) + } + /> + ); +} + +export default PowerBIEmbed; diff --git a/app/components/PowerBiEmbed/styles.module.css b/app/components/PowerBiEmbed/styles.module.css new file mode 100644 index 0000000..34c3e71 --- /dev/null +++ b/app/components/PowerBiEmbed/styles.module.css @@ -0,0 +1,7 @@ +.embed { + aspect-ratio: 5 / 3; + + iframe { + border: none; + } +} diff --git a/app/components/PreloadMessage/index.tsx b/app/components/PreloadMessage/index.tsx new file mode 100644 index 0000000..f68d626 --- /dev/null +++ b/app/components/PreloadMessage/index.tsx @@ -0,0 +1,17 @@ +import styles from './styles.module.css'; + +interface Props { + children?: React.ReactNode; +} + +function PreloadMessage(props: Props) { + const { children } = props; + + return ( +
+ {children} +
+ ); +} + +export default PreloadMessage; diff --git a/app/components/PreloadMessage/styles.module.css b/app/components/PreloadMessage/styles.module.css new file mode 100644 index 0000000..a71eda0 --- /dev/null +++ b/app/components/PreloadMessage/styles.module.css @@ -0,0 +1,11 @@ +.preload-message { + display: flex; + align-items: center; + flex-direction: column; + flex-grow: 1; + justify-content: center; + background-color: var(--go-ui-color-background); + height: 100vh; + text-align: center; + font-size: var(--go-ui-font-size-lg); +} diff --git a/app/components/RegionSelectInput/index.tsx b/app/components/RegionSelectInput/index.tsx new file mode 100644 index 0000000..5916238 --- /dev/null +++ b/app/components/RegionSelectInput/index.tsx @@ -0,0 +1,48 @@ +import { SelectInput } from '@ifrc-go/ui'; + +import { + keySelector, + labelSelector, + type Selector, +} from '#utils/utils'; + +// Note: This will dynamically fetch from server +const ethiopiaRegions: Selector[] = [ + { key: 'AA', label: 'Addis Ababa' }, + { key: 'AF', label: 'Afar' }, + { key: 'AM', label: 'Amhara' }, + { key: 'BE', label: 'Benishangul-Gumuz' }, + { key: 'CERS', label: 'Central Ethiopia Regional State' }, + { key: 'DR', label: 'Dire Dawa' }, + { key: 'GA', label: 'Gambela' }, + { key: 'HA', label: 'Harari' }, + { key: 'OR', label: 'Oromia' }, + { key: 'SI', label: 'Sidama' }, + { key: 'SO', label: 'Somali' }, + { key: 'SW', label: 'South West Ethiopia' }, + { key: 'SNNP', label: 'SNNPR' }, + { key: 'TI', label: 'Tigray' }, +]; + +type Props = { + name: string; + value: string | undefined; + onChange: (value: string | undefined, name: string) => void; +}; + +function RegionSelectInput(props: Props) { + const { name, value, onChange } = props; + return ( + + ); +} + +export default RegionSelectInput; diff --git a/app/components/ReportCard/index.tsx b/app/components/ReportCard/index.tsx new file mode 100644 index 0000000..ab11230 --- /dev/null +++ b/app/components/ReportCard/index.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import { DashboardLineIcon } from '@ifrc-go/icons'; +import { + Description, + Heading, + Image, + InlineLayout, + ListView, +} from '@ifrc-go/ui'; + +import type { ReportsQuery } from '#generated/types/graphql'; + +import styles from './styles.module.css'; + +type Report = NonNullable['results'][number] +type ReportCardProps = { + report: Report; +} +function ReportCard({ report }: ReportCardProps) { + const { + coverImage, + title, + description, + } = report; + + return ( + + ) : ( + + + + ) + } + contentAlignment="start" + spacing="lg" + > + + + {title} + + + {description} + + + + ); +} + +export default ReportCard; diff --git a/app/components/ReportCard/styles.module.css b/app/components/ReportCard/styles.module.css new file mode 100644 index 0000000..11aad45 --- /dev/null +++ b/app/components/ReportCard/styles.module.css @@ -0,0 +1,16 @@ +.cover-image, +.default-cover-image { + width: 8.75rem; + height: 8.75rem; +} + +.default-cover-image { + background-color: var(--go-ui-color-red-10); + color: var(--go-ui-color-primary-red); + + .dashboard-icon { + width: 2.75rem; + height: 2.75rem; + } +} + diff --git a/app/components/WikiLink/index.tsx b/app/components/WikiLink/index.tsx new file mode 100644 index 0000000..a6fa76b --- /dev/null +++ b/app/components/WikiLink/index.tsx @@ -0,0 +1,32 @@ +import { WikiHelpSectionLineIcon } from '@ifrc-go/icons'; +import { _cs } from '@togglecorp/fujs'; + +import Link from '#components/Link'; + +import styles from './styles.module.css'; + +interface Props { + pathName: string; + className?: string; +} + +function WikiLink(props: Props) { + const { + pathName, + className, + } = props; + + return ( + + + + ); +} + +export default WikiLink; diff --git a/app/components/WikiLink/styles.module.css b/app/components/WikiLink/styles.module.css new file mode 100644 index 0000000..df2d7c1 --- /dev/null +++ b/app/components/WikiLink/styles.module.css @@ -0,0 +1,6 @@ +.wiki-link { + .icon { + line-height: 1; + font-size: var(--go-ui-font-size-2xl); + } +} diff --git a/app/config.ts b/app/config.ts new file mode 100644 index 0000000..2427959 --- /dev/null +++ b/app/config.ts @@ -0,0 +1,19 @@ +const { + APP_TITLE, + APP_ENVIRONMENT, + APP_GRAPHQL_ENDPOINT, + APP_GO_URL, + APP_GO_API, + APP_MAPBOX_TOKEN, + APP_GRAPHQL_CODEGEN_ENDPOINT, + APP_GO_RISK_API_ENDPOINT, +} = import.meta.env; + +export const environment = APP_ENVIRONMENT; +export const appTitle = APP_TITLE; +export const api = APP_GRAPHQL_ENDPOINT; +export const goApi = APP_GO_API; +export const riskApi = APP_GO_RISK_API_ENDPOINT; +export const mapboxToken = APP_MAPBOX_TOKEN; +export const codegen = APP_GRAPHQL_CODEGEN_ENDPOINT; +export const goUrl = APP_GO_URL; diff --git a/app/contexts/GoContext.ts b/app/contexts/GoContext.ts new file mode 100644 index 0000000..7387311 --- /dev/null +++ b/app/contexts/GoContext.ts @@ -0,0 +1,32 @@ +import { createContext } from 'react'; + +import type { GoApiResponse } from '#utils/restRequest'; + +export type GlobalEnums = Partial>; +export type CountryResponse = GoApiResponse<'/api/v2/country/{id}/'> +export type DisasterTypes = GoApiResponse<'/api/v2/disaster_type/'>; + +export interface GoContextInterface { + countryId: number ; + + countryResponse: CountryResponse | undefined; + countryResponsePending: boolean; + + disasterTypes?: DisasterTypes; + disasterTypesPending?: boolean; + + globalEnums?: GlobalEnums; + globalEnumsPending?: boolean; +} + +const GoContext = createContext({ + countryId: 0, + countryResponse: undefined, + countryResponsePending: false, + disasterTypes: undefined, + disasterTypesPending: false, + globalEnums: undefined, + globalEnumsPending: false, +}); + +export default GoContext; diff --git a/app/contexts/UserContext.ts b/app/contexts/UserContext.ts index c8799dd..1141f73 100644 --- a/app/contexts/UserContext.ts +++ b/app/contexts/UserContext.ts @@ -1,10 +1,10 @@ -import type { User } from '#root/types/user'; import { createContext } from 'react'; +import type { MeQuery } from '#generated/types/graphql'; export interface UserContextInterface { - user: User | undefined; - setUser: React.Dispatch>; + user: MeQuery['me'] | undefined; + setUser: React.Dispatch>; authenticated: boolean, } diff --git a/app/declarations/env.d.ts b/app/declarations/env.d.ts new file mode 100644 index 0000000..7001d49 --- /dev/null +++ b/app/declarations/env.d.ts @@ -0,0 +1,15 @@ +/// + +type ImportMetaEnvAugmented = import('@togglecorp/vite-plugin-validate-env').ImportMetaEnvAugmented< + typeof import('../../env').default +> + +interface ImportMetaEnv extends ImportMetaEnvAugmented { + // The custom environment variables that are passed through the vite + APP_COMMIT_HASH: string; + APP_VERSION: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} diff --git a/app/declarations/turf.d.ts b/app/declarations/turf.d.ts new file mode 100644 index 0000000..f88a728 --- /dev/null +++ b/app/declarations/turf.d.ts @@ -0,0 +1 @@ +declare module '@turf/bbox'; diff --git a/app/hooks/useAlert.ts b/app/hooks/useAlert.ts new file mode 100644 index 0000000..328790e --- /dev/null +++ b/app/hooks/useAlert.ts @@ -0,0 +1,49 @@ +import { + useCallback, + useContext, + useMemo, +} from 'react'; +import { + AlertContext, + type AlertType, +} from '@ifrc-go/ui/contexts'; +import { DURATION_DEFAULT_ALERT_DISMISS } from '@ifrc-go/ui/utils'; +import { randomString } from '@togglecorp/fujs'; + +interface AddAlertOption { + name?: string; + variant?: AlertType; + duration?: number; + description?: React.ReactNode; + nonDismissable?: boolean; + debugMessage?: string; +} + +function useAlert() { + const { + addAlert, + // removeAlert, + // updateAlert, + } = useContext(AlertContext); + + const show = useCallback((title: React.ReactNode, options?: AddAlertOption) => { + const name = options?.name ?? randomString(16); + addAlert({ + variant: options?.variant ?? 'info', + duration: options?.duration ?? DURATION_DEFAULT_ALERT_DISMISS, + name: options?.name ?? name, + title, + description: options?.description, + nonDismissable: options?.nonDismissable ?? false, + debugMessage: options?.debugMessage, + }); + + return name; + }, [addAlert]); + + return useMemo(() => ({ + show, + }), [show]); +} + +export default useAlert; diff --git a/app/hooks/useDebouncedValue.ts b/app/hooks/useDebouncedValue.ts new file mode 100644 index 0000000..5cb59a4 --- /dev/null +++ b/app/hooks/useDebouncedValue.ts @@ -0,0 +1,34 @@ +import { + useEffect, + useState, +} from 'react'; + +function useDebouncedValue( + input: T, + debounceTime?: number, +): T +function useDebouncedValue( + input: T, + debounceTime: number | undefined, + transformer: (value: T) => V, +): V +function useDebouncedValue( + input: T, + debounceTime?: number, + transformer?: (value: T) => V, +) { + const [debounceValue, setDebouncedValue] = useState( + () => (transformer ? transformer(input) : input), + ); + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(transformer ? transformer(input) : input); + }, debounceTime ?? 300); + return () => { + clearTimeout(handler); + }; + }, [input, debounceTime, transformer]); + return debounceValue; +} + +export default useDebouncedValue; diff --git a/app/hooks/useFilterState.ts b/app/hooks/useFilterState.ts new file mode 100644 index 0000000..d6b2292 --- /dev/null +++ b/app/hooks/useFilterState.ts @@ -0,0 +1,213 @@ +import { + type SetStateAction, + useCallback, + useMemo, + useReducer, +} from 'react'; +import { useDebouncedValue } from '@ifrc-go/ui/hooks'; +import { hasSomeDefinedValue } from '@ifrc-go/ui/utils'; +import { isNotDefined } from '@togglecorp/fujs'; +import { type EntriesAsList } from '@togglecorp/toggle-form'; + +type SortDirection = 'asc' | 'dsc'; +interface SortParameter { + name: string; + direction: SortDirection; +} +function getOrdering(sorting: SortParameter | undefined) { + if (isNotDefined(sorting)) { + return undefined; + } + if (sorting.direction === 'asc') { + return sorting.name; + } + return `-${sorting.name}`; +} + +interface ResetFilterAction { + type: 'reset-filter'; +} + +interface SetFilterAction { + type: 'set-filter'; + value: SetStateAction; +} + +interface SetPageAction { + type: 'set-page'; + value: number; +} + +interface SetOrderingAction { + type: 'set-ordering' + value: SetStateAction; +} + +type FilterActions = ( + ResetFilterAction + | SetFilterAction + | SetPageAction + | SetOrderingAction +); + +interface FilterState { + filter: FILTER, + ordering: SortParameter | undefined, + page: number, +} + +const defaultOrdering: SortParameter = { + name: 'id', + direction: 'dsc', +}; + +function useFilterState(options: { + filter: FILTER, + ordering?: SortParameter | undefined, + page?: number, + pageSize?: number, + debounceTime?: number, +}) { + const { + filter, + ordering = defaultOrdering, + page = 1, + pageSize = 10, + debounceTime = 300, + } = options; + + const [state, dispatch] = useReducer, [FilterActions]>( + (prevState, action) => { + if (action.type === 'reset-filter') { + return { + filter, + ordering, + page, + }; + } + if (action.type === 'set-filter') { + return { + ...prevState, + filter: typeof action.value === 'function' + ? action.value(prevState.filter) + : action.value, + page: 1, + }; + } + if (action.type === 'set-page') { + return { + ...prevState, + page: action.value, + }; + } + if (action.type === 'set-ordering') { + return { + ...prevState, + ordering: typeof action.value === 'function' + ? action.value(prevState.ordering) + : action.value, + page: 1, + }; + } + return prevState; + }, + { + filter, + ordering, + page, + }, + ); + + const setFilter = useCallback( + (value: SetStateAction) => { + dispatch({ + type: 'set-filter', + value, + }); + }, + [], + ); + + const resetFilter = useCallback( + () => { + dispatch({ type: 'reset-filter' }); + }, + [], + ); + + const setFilterField = useCallback( + (...args: EntriesAsList) => { + const [val, key] = args; + setFilter((oldFilterValue) => { + const newFilterValue = { + ...oldFilterValue, + [key]: val, + }; + return newFilterValue; + }); + }, + [setFilter], + ); + + const setPage = useCallback( + (value: number) => { + dispatch({ + type: 'set-page', + value, + }); + }, + [], + ); + const setOrdering = useCallback( + (value: SetStateAction) => { + dispatch({ + type: 'set-ordering', + value, + }); + }, + [], + ); + + const debouncedState = useDebouncedValue(state, debounceTime); + + const sortState = useMemo( + () => ({ + sorting: state.ordering, + setSorting: setOrdering, + }), + [state.ordering, setOrdering], + ); + + const filtered = useMemo( + () => hasSomeDefinedValue(debouncedState.filter), + [debouncedState.filter], + ); + const rawFiltered = useMemo( + () => hasSomeDefinedValue(state.filter), + [state.filter], + ); + + return { + rawFilter: state.filter, + rawFiltered, + + filter: debouncedState.filter, + filtered, + setFilter, + setFilterField, + + resetFilter, + + page: state.page, + offset: pageSize * (debouncedState.page - 1), + limit: pageSize, + setPage, + + rawOrdering: getOrdering(ordering), + ordering: getOrdering(debouncedState.ordering), + + sortState, + }; +} + +export default useFilterState; diff --git a/app/hooks/useInputState.ts b/app/hooks/useInputState.ts new file mode 100644 index 0000000..f731be6 --- /dev/null +++ b/app/hooks/useInputState.ts @@ -0,0 +1,43 @@ +import React, { + useEffect, + useRef, +} from 'react'; + +type ValueOrSetterFn = T | ((value: T) => T); +function isSetterFn(value: ValueOrSetterFn): value is ((value: T) => T) { + return typeof value === 'function'; +} + +function useInputState( + initialValue: T, + sideEffect?: (newValue: T, oldValue: T) => T, +) { + const [value, setValue] = React.useState(initialValue); + const sideEffectRef = useRef(sideEffect); + + useEffect( + () => { + sideEffectRef.current = sideEffect; + }, + [sideEffect], + ); + + type SetValue = React.Dispatch>; + const setValueSafe: SetValue = React.useCallback((newValueOrSetter) => { + setValue((oldValue) => { + const newValue = isSetterFn(newValueOrSetter) + ? newValueOrSetter(oldValue) + : newValueOrSetter; + + if (sideEffectRef.current) { + return sideEffectRef.current(newValue, oldValue); + } + + return newValue; + }); + }, []); + + return [value, setValueSafe] as const; +} + +export default useInputState; diff --git a/app/hooks/useRecursiveCsvRequest.ts b/app/hooks/useRecursiveCsvRequest.ts new file mode 100644 index 0000000..066dbf7 --- /dev/null +++ b/app/hooks/useRecursiveCsvRequest.ts @@ -0,0 +1,276 @@ +import { + useCallback, + useRef, + useState, +} from 'react'; +import { + isDefined, + isFalsyString, + isNotDefined, +} from '@togglecorp/fujs'; +import Papa from 'papaparse'; + +import { + goApi, + riskApi, +} from '#config'; +import { resolveUrl } from '#utils/resolveUrl'; + +type Maybe = T | null | undefined; + +interface UrlParams { + [key: string]: Maybe; +} + +function prepareUrlParams(params: UrlParams): string { + const finalParams: UrlParams = { + ...params, + format: 'csv', + }; + return Object.keys(finalParams) + .filter((k) => isDefined(finalParams[k])) + .map((k) => { + const param = finalParams[k]; + if (isNotDefined(param)) { + return undefined; + } + let val: string; + if (Array.isArray(param)) { + val = param.join(','); + } else if (typeof param === 'number' || typeof param === 'boolean') { + val = String(param); + } else { + val = param; + } + return `${encodeURIComponent(k)}=${encodeURIComponent(val)}`; + }) + .filter(isDefined) + .join('&'); +} + +const prepareUrl = (url: string, apiType = 'go') => { + if (isFalsyString(url)) { + return ''; + } + + // external URL + if (/^https?:\/\//i.test(url)) { + return url; + } + + const baseUrl = apiType === 'risk' ? riskApi : goApi; + + if (isFalsyString(baseUrl)) { + return ''; + } + + return resolveUrl(baseUrl, url); +}; + +const PAGE_SIZE = 100; + +async function wait(time: number) { + return new Promise((resolve) => { + setTimeout(() => resolve(true), time); + }); +} + +async function fetchData(url: string, urlParams: UrlParams) { + const finalUrl = `${prepareUrl(url)}?${prepareUrlParams(urlParams)}`; + const response = await fetch( + finalUrl, + { + method: 'GET', + headers: { + 'Content-Type': 'text/csv; charset=utf-8', + 'Accept-Language': 'en', + }, + }, + ); + const finalText = await response.text(); + + return { + data: finalText, + status: response.status, + }; +} + +async function fetchRecursive({ + url, + urlParams, + onPartialSuccess, + onSuccess, + offset, + pageLimit, + onFailure, + totalCount, + noOfRetries = 0, +} : { + url: string, + urlParams: UrlParams, + onPartialSuccess: (data: string) => void, + onSuccess: () => void, + offset: number, + pageLimit: number, + onFailure: (error: unknown) => void, + totalCount: number, + noOfRetries?: number, +}) { + let response; + + try { + response = await fetchData( + url, + { + ...urlParams, + offset, + limit: pageLimit, + }, + ); + } catch { + onFailure('Failed to fetch data'); + return; + } + + if (response.status >= 200 && response.status <= 299) { + onPartialSuccess(response.data); + + const newOffset = offset + pageLimit; + + if (newOffset < totalCount) { + await wait((noOfRetries ** 2) * 500 + Math.random() * 200); + await fetchRecursive({ + url, + urlParams, + onPartialSuccess, + offset: newOffset, + pageLimit, + onSuccess, + onFailure, + totalCount, + noOfRetries: Math.max(0, noOfRetries - 1), + }); + } else { + onSuccess(); + } + } else if (response.status === 429) { + await wait((noOfRetries ** 2) * 1000 + Math.random() * 1000); + await fetchRecursive({ + url, + urlParams, + onPartialSuccess, + offset, + pageLimit, + onSuccess, + onFailure, + totalCount, + noOfRetries: noOfRetries + 1, + }); + } else { + onFailure(response?.data); + } +} + +function useRecursiveCSVRequest({ + onFailure, + onSuccess, + disableProgress, +} : { + onFailure: (error: unknown) => void; + onSuccess: (data: D[], total: number) => void; + disableProgress?: boolean, +}) { + const [pending, setPending] = useState(false); + const [progress, setProgress] = useState(0); + + const dataRef = useRef([]); + const totalRef = useRef(0); + + const handleFailure = useCallback((error: unknown) => { + dataRef.current = []; + totalRef.current = 0; + + setPending(false); + setProgress(0); + + onFailure(error); + }, [onFailure]); + + const handleSuccess = useCallback(() => { + const data = dataRef.current; + const total = totalRef.current; + + dataRef.current = []; + totalRef.current = 0; + + setPending(false); + setProgress(0); + + if (total !== data.length - 1 && !disableProgress) { + // eslint-disable-next-line no-console + console.error(`Length mismatch. Expected ${total} but got ${data.length - 1}`); + onFailure(undefined); + } else { + onSuccess(data, total); + } + }, [onSuccess, onFailure, setPending, setProgress, disableProgress]); + + const handlePartialSuccess = useCallback((newResponse: string) => { + Papa.parse( + newResponse, + { + skipEmptyLines: true, + complete: (test: { data: D[] }) => { + const items = [...test.data]; + + // NOTE: meaning this is not the first request + if (dataRef.current.length > 0) { + // NOTE: remove the headers + items.shift(); + } + + dataRef.current = [ + ...dataRef.current, + ...items, + ]; + + if (totalRef.current === 0 || dataRef.current.length === 0) { + setProgress(0); + } else if (disableProgress) { + setProgress(0); + } else { + // NOTE: we are negating one because we have a header as well + setProgress((dataRef.current.length - 1) / totalRef.current); + } + }, + }, + ); + }, [disableProgress]); + + const trigger = useCallback((url: string, total: number, newUrlParams: UrlParams) => { + dataRef.current = []; + totalRef.current = total; + + setPending(true); + setProgress(0); + + fetchRecursive({ + url, + urlParams: newUrlParams, + offset: 0, + pageLimit: PAGE_SIZE, + onPartialSuccess: handlePartialSuccess, + onSuccess: handleSuccess, + onFailure: handleFailure, + totalCount: total, + }); + }, [ + handleSuccess, + handlePartialSuccess, + handleFailure, + ]); + + return [pending, progress, trigger] as const; +} + +export default useRecursiveCSVRequest; diff --git a/app/hooks/useSetFieldValue.ts b/app/hooks/useSetFieldValue.ts new file mode 100644 index 0000000..5180409 --- /dev/null +++ b/app/hooks/useSetFieldValue.ts @@ -0,0 +1,25 @@ +import { useCallback } from 'react'; +import { + type EntriesAsList, + isCallable, +} from '@togglecorp/toggle-form'; + +function useSetFieldValue( + setValue: React.Dispatch>, +) { + const setFieldValue = useCallback((...entries: EntriesAsList) => { + setValue((oldState) => { + const newValue = isCallable(entries[0]) + ? entries[0](oldState[entries[1]]) + : entries[0]; + return { + ...oldState, + [entries[1]]: newValue, + }; + }); + }, [setValue]); + + return setFieldValue; +} + +export default useSetFieldValue; diff --git a/app/hooks/useTemporalChartData.ts b/app/hooks/useTemporalChartData.ts new file mode 100644 index 0000000..133dc78 --- /dev/null +++ b/app/hooks/useTemporalChartData.ts @@ -0,0 +1,661 @@ +import { + useCallback, + useMemo, + useRef, +} from 'react'; +import { useSizeTracking } from '@ifrc-go/ui/hooks'; +import { + type Bounds, + type ChartScale, + type DateLike, + formatNumber, + getBounds, + getChartDimensions, + getEvenDistribution, + getIntervals, + getNumberOfDays, + getNumberOfMonths, + getScaleFunction, + getTemporalDiff, + maxSafe, + minSafe, + type Rect, +} from '@ifrc-go/ui/utils'; +import { + bound, + compareNumber, + isDefined, + isNotDefined, +} from '@togglecorp/fujs'; + +import { + DEFAULT_X_AXIS_HEIGHT, + DEFAULT_Y_AXIS_WIDTH, + defaultChartMargin, + defaultChartPadding, + NUM_X_AXIS_TICKS_MAX, + NUM_X_AXIS_TICKS_MIN, +} from '#utils/constants'; + +type TemporalResolution = 'year' | 'month' | 'day'; + +interface CommonOptions { + keySelector: (d: DATUM, index: number) => number | string; + xValueSelector: (d: DATUM, index: number) => DateLike | undefined | null; + yValueSelector: (d: DATUM, index: number) => number | undefined | null; + xAxisHeight?: number; + yAxisWidth?: number; + chartMargin?: Rect; + chartPadding?: Rect; + numYAxisTicks?: number; + yAxisTickLabelSelector?: (value: number, index: number) => React.ReactNode; + yValueStartsFromZero?: boolean; + yScale?: ChartScale; + yDomain?: Bounds; +} + +interface NonYearlyChartOptions { + yearlyChart?: never; + temporalResolution?: 'auto' | TemporalResolution; + // NOTE: should be between 3 - 12 + numXAxisTicks?: 'auto' | number; +} + +interface YearlyChartOptions { + yearlyChart: true; + temporalResolution?: never; + numXAxisTicks?: never; +} + +type Options = CommonOptions & ( + NonYearlyChartOptions | YearlyChartOptions +) + +function useTemporalChartData(data: DATUM[] | undefined | null, options: Options) { + const { + keySelector, + xValueSelector, + yValueSelector, + temporalResolution: temporalResolutionFromProps = 'auto', + numXAxisTicks: numXAxisTicksFromProps = 'auto', + yAxisWidth = DEFAULT_Y_AXIS_WIDTH, + xAxisHeight = DEFAULT_X_AXIS_HEIGHT, + chartMargin = defaultChartMargin, + chartPadding = defaultChartPadding, + numYAxisTicks = 6, + yAxisTickLabelSelector, + yearlyChart = false, + yValueStartsFromZero, + yScale = 'linear', + yDomain, + } = options; + + const containerRef = useRef(null); + + const handleRefChange = useCallback( + (element: HTMLDivElement | null) => { + containerRef.current = element; + }, + [], + ); + + const chartSize = useSizeTracking(containerRef as React.RefObject); + + const chartData = useMemo( + () => data?.map( + (datum, i) => { + const key = keySelector(datum, i); + const xValue = xValueSelector(datum, i); + const yValue = yValueSelector(datum, i); + + if (isNotDefined(xValue) || isNotDefined(yValue)) { + return undefined; + } + + return { + key, + originalData: datum, + xValue, + yValue, + }; + }, + ).filter(isDefined) ?? [], + [data, keySelector, xValueSelector, yValueSelector], + ); + + const dataDomain = useMemo( + () => { + if (isNotDefined(chartData) || chartData.length === 0) { + return undefined; + } + + const timestampList = chartData.map(({ xValue }) => { + const date = new Date(xValue); + + if (Number.isNaN(date.getTime())) { + return undefined; + } + + return date.getTime(); + }).filter(isDefined); + + const dataMinTimestamp = minSafe(timestampList); + const dataMaxTimestamp = maxSafe(timestampList); + + if (isNotDefined(dataMinTimestamp) || isNotDefined(dataMaxTimestamp)) { + return undefined; + } + + return { + min: dataMinTimestamp, + max: dataMaxTimestamp, + }; + }, + [chartData], + ); + + const dataTemporalDiff = useMemo | undefined>( + () => { + if (isNotDefined(dataDomain)) { + return undefined; + } + + return getTemporalDiff(dataDomain.min, dataDomain.max); + }, + [dataDomain], + ); + + const temporalResolution = useMemo( + () => { + if (yearlyChart) { + return 'month'; + } + + if ( + temporalResolutionFromProps !== 'auto' + && ( + temporalResolutionFromProps === 'day' + || temporalResolutionFromProps === 'month' + || temporalResolutionFromProps === 'year' + ) + ) { + return temporalResolutionFromProps; + } + + // NOTE: revisit + if (isNotDefined(dataTemporalDiff)) { + return 'day'; + } + + if (dataTemporalDiff.year > NUM_X_AXIS_TICKS_MIN) { + return 'year'; + } + + if (dataTemporalDiff.month > NUM_X_AXIS_TICKS_MIN) { + return 'month'; + } + + return 'day'; + }, + [dataTemporalDiff, temporalResolutionFromProps, yearlyChart], + ); + + const numXAxisTicks = useMemo( + () => { + if (yearlyChart) { + return 12; + } + + if (numXAxisTicksFromProps !== 'auto') { + return bound(numXAxisTicksFromProps, NUM_X_AXIS_TICKS_MIN, NUM_X_AXIS_TICKS_MAX); + } + + if (isNotDefined(dataTemporalDiff)) { + return NUM_X_AXIS_TICKS_MIN; + } + + const currentDiff = dataTemporalDiff[temporalResolution] + 2; + + if (currentDiff <= NUM_X_AXIS_TICKS_MIN) { + return NUM_X_AXIS_TICKS_MIN; + } + + const tickRange = NUM_X_AXIS_TICKS_MAX - NUM_X_AXIS_TICKS_MIN; + const numTicksList = Array.from(Array(tickRange + 1).keys()).map( + (key) => NUM_X_AXIS_TICKS_MIN + key, + ); + + const potentialTicks = numTicksList.reverse().map( + (numTicks) => { + const tickDiff = Math.ceil(currentDiff / numTicks); + const offset = numTicks * tickDiff - currentDiff; + + return { + numTicks, + offset, + rank: numTicks / (offset + 3), + }; + }, + ); + + const tickWithLowestOffset = [...potentialTicks].sort( + (a, b) => compareNumber(a.rank, b.rank, -1), + )[0]!; + + return bound( + tickWithLowestOffset.numTicks, + NUM_X_AXIS_TICKS_MIN, + NUM_X_AXIS_TICKS_MAX, + ); + }, + [numXAxisTicksFromProps, dataTemporalDiff, temporalResolution, yearlyChart], + ); + + const chartDomain = useMemo( + () => { + if (yearlyChart) { + const now = new Date(); + + return { + min: new Date(now.getFullYear(), 0, 1), + max: new Date(now.getFullYear(), 11, 1), + }; + } + + if (isNotDefined(dataDomain) || isNotDefined(dataTemporalDiff)) { + const now = new Date(); + + if (temporalResolution === 'year') { + return { + min: new Date( + now.getFullYear() - numXAxisTicks, + now.getMonth(), + now.getDate(), + ), + max: now, + }; + } + + if (temporalResolution === 'month') { + now.setDate(1); + + return { + min: new Date( + now.getFullYear(), + now.getMonth() - numXAxisTicks, + now.getDate(), + ), + max: now, + }; + } + + return { + min: new Date( + now.getFullYear(), + now.getMonth(), + now.getDate() - numXAxisTicks, + ), + max: now, + }; + } + + const minDataDate = new Date(dataDomain.min); + const maxDataDate = new Date(dataDomain.max); + + if (temporalResolution === 'year') { + const { left, right } = getEvenDistribution( + minDataDate.getFullYear(), + maxDataDate.getFullYear(), + numXAxisTicks, + ); + + return { + min: new Date(minDataDate.getFullYear() - left, 0, 1), + max: new Date(maxDataDate.getFullYear() + right, 0, 1), + }; + } + + if (temporalResolution === 'month') { + const { left, right } = getEvenDistribution( + 0, + dataTemporalDiff.month, + numXAxisTicks, + ); + + return { + min: new Date( + minDataDate.getFullYear(), + minDataDate.getMonth() - left, + 1, + ), + max: new Date( + maxDataDate.getFullYear(), + maxDataDate.getMonth() + right, + 1, + ), + }; + } + + const { left, right } = getEvenDistribution( + 0, + getNumberOfDays(minDataDate, maxDataDate), + numXAxisTicks, + ); + + return { + min: new Date( + minDataDate.getFullYear(), + minDataDate.getMonth(), + minDataDate.getDate() - left, + ), + max: new Date( + maxDataDate.getFullYear(), + maxDataDate.getMonth(), + maxDataDate.getDate() + right, + ), + }; + }, + [temporalResolution, dataDomain, numXAxisTicks, yearlyChart, dataTemporalDiff], + ); + + const chartTemporalDiff = useMemo>( + () => getTemporalDiff(chartDomain.min, chartDomain.max), + [chartDomain], + ); + + const getRelativeX = useCallback( + (dateLike: DateLike) => { + const date = new Date(dateLike); + const monthDiff = getNumberOfMonths(chartDomain.min, date); + + if (temporalResolution === 'year') { + const yearDiff = date.getFullYear() - chartDomain.min.getFullYear(); + + return yearDiff + (monthDiff - yearDiff * 12) / 12; + } + + if (temporalResolution === 'month') { + return monthDiff; + } + + return getNumberOfDays(chartDomain.min, date); + }, + [chartDomain, temporalResolution], + ); + + const { + dataAreaSize, + dataAreaOffset, + } = useMemo( + () => getChartDimensions({ + chartSize, + chartMargin, + chartPadding, + xAxisHeight, + yAxisWidth, + numXAxisTicks, + }), + [chartSize, chartMargin, chartPadding, xAxisHeight, yAxisWidth, numXAxisTicks], + ); + + const xAxisDomain = useMemo( + () => { + if (yearlyChart) { + return { + min: 0, + max: 11, + }; + } + + if (isNotDefined(dataTemporalDiff)) { + return { + min: 0, + max: numXAxisTicks, + }; + } + + if (temporalResolution === 'year') { + return { + min: 0, + max: chartDomain.max.getFullYear() - chartDomain.min.getFullYear(), + }; + } + + if (temporalResolution === 'month') { + return { + min: 0, + max: getNumberOfMonths(chartDomain.min, chartDomain.max), + }; + } + + return { + min: 0, + max: getNumberOfDays(chartDomain.min, chartDomain.max), + }; + }, + [chartDomain, temporalResolution, yearlyChart, dataTemporalDiff, numXAxisTicks], + ); + + const xScaleFnRelative = useMemo( + () => getScaleFunction( + xAxisDomain, + { min: 0, max: chartSize.width }, + { start: dataAreaOffset.left, end: dataAreaOffset.right }, + ), + [dataAreaOffset, xAxisDomain, chartSize.width], + ); + + const xScaleFn = useCallback( + (value: DateLike) => { + if (yearlyChart) { + const now = new Date(); + const date = new Date(value); + return xScaleFnRelative( + getRelativeX(new Date(now.getFullYear(), date.getMonth(), 1)), + ); + } + + return xScaleFnRelative(getRelativeX(value)); + }, + [xScaleFnRelative, getRelativeX, yearlyChart], + ); + + const yAxisDomain = useMemo( + () => { + if (isDefined(yDomain)) { + return yDomain; + } + + const yValues = chartData.map(({ yValue }) => yValue); + const yBounds = getBounds(yValues); + + if (yValueStartsFromZero) { + const { left, right } = getEvenDistribution( + 0, + yBounds.max, + numYAxisTicks, + ); + + return { + min: 0, + max: yBounds.max + left + right, + }; + } + + const { left, right } = getEvenDistribution( + yBounds.min, + yBounds.max, + numYAxisTicks, + ); + + return { + min: yBounds.min - left, + max: yBounds.max + right, + }; + }, + [chartData, yValueStartsFromZero, yDomain, numYAxisTicks], + ); + + const yScaleFn = useMemo( + () => getScaleFunction( + yAxisDomain, + { min: 0, max: chartSize.height }, + { start: dataAreaOffset.top, end: dataAreaOffset.bottom }, + true, + yScale, + ), + [yAxisDomain, dataAreaOffset, chartSize.height, yScale], + ); + + const chartPoints = useMemo( + () => chartData.map( + (datum) => ({ + ...datum, + x: xScaleFn(datum.xValue), + y: yScaleFn(datum.yValue), + }), + ), + [chartData, xScaleFn, yScaleFn], + ); + + const xAxisTicks = useMemo( + () => { + const diff = yearlyChart ? 12 : chartTemporalDiff[temporalResolution]; + + const step = Math.max(Math.ceil(diff / numXAxisTicks), 1); + const ticks = Array.from(Array(numXAxisTicks).keys()).map( + (key) => key * step, + ); + + if (yearlyChart) { + const currentYear = new Date().getFullYear(); + + return ticks.map((tick) => { + const date = new Date( + currentYear, + tick, + 1, + ); + + return { + key: tick, + x: xScaleFnRelative(tick), + label: date.toLocaleString( + 'default', + { month: 'short' }, + ), + }; + }); + } + + if (temporalResolution === 'year') { + return ticks.map((tick) => ({ + key: tick, + x: xScaleFnRelative(tick), + label: chartDomain.min.getFullYear() + tick, + })); + } + + if (temporalResolution === 'month') { + return ticks.map((tick) => { + const date = new Date( + chartDomain.min.getFullYear(), + chartDomain.min.getMonth() + tick, + 1, + ); + + return { + key: tick, + x: xScaleFnRelative(tick), + label: date.toLocaleString( + 'default', + { + year: 'numeric', + month: 'short', + }, + ), + }; + }); + } + + return ticks.map((tick) => { + const date = new Date( + chartDomain.min.getFullYear(), + chartDomain.min.getMonth(), + chartDomain.min.getDate() + tick, + ); + + return { + key: tick, + x: xScaleFnRelative(tick), + label: date.toLocaleString( + 'default', + { + year: 'numeric', + month: 'short', + day: '2-digit', + }, + ), + }; + }); + }, + [ + numXAxisTicks, + temporalResolution, + chartDomain.min, + xScaleFnRelative, + yearlyChart, + chartTemporalDiff, + ], + ); + + const yAxisTicks = useMemo( + () => getIntervals( + yAxisDomain, + numYAxisTicks, + ).map((tick, i) => ({ + y: yScaleFn(tick), + label: yAxisTickLabelSelector + ? yAxisTickLabelSelector(tick, i) + : formatNumber(tick, { compact: true }) ?? '', + })), + [yAxisDomain, yScaleFn, numYAxisTicks, yAxisTickLabelSelector], + ); + + return useMemo( + () => ({ + chartPoints, + xAxisTicks, + yAxisTicks, + chartSize, + xScaleFn, + yScaleFn, + dataAreaSize, + dataAreaOffset, + xAxisHeight, + yAxisWidth, + chartMargin, + temporalResolution, + numXAxisTicks, + containerRef: handleRefChange, + }), + [ + chartPoints, + xAxisTicks, + yAxisTicks, + chartSize, + xScaleFn, + yScaleFn, + dataAreaSize, + dataAreaOffset, + chartMargin, + temporalResolution, + numXAxisTicks, + xAxisHeight, + yAxisWidth, + handleRefChange, + ], + ); +} + +export default useTemporalChartData; diff --git a/app/hooks/useUrlSearchState.ts b/app/hooks/useUrlSearchState.ts new file mode 100644 index 0000000..d0560b4 --- /dev/null +++ b/app/hooks/useUrlSearchState.ts @@ -0,0 +1,92 @@ +import { + useCallback, + useEffect, + useMemo, + useRef, +} from 'react'; +import { + type NavigateOptions, + useSearchParams, +} from 'react-router'; +import { + encodeDate, + isNotDefined, +} from '@togglecorp/fujs'; +import { isCallable } from '@togglecorp/toggle-form'; + +type SearchValueFromUrl = string | null | undefined; +type SearchValueFromUser = string | number | boolean | Date | undefined | null; + +type ValueOrSetter = VALUE | ((prevValue: VALUE) => VALUE); + +function useUrlSearchState( + key: string, + deserialize: (value: SearchValueFromUrl) => VALUE, + serialize: (value: VALUE) => SearchValueFromUser, + navigateOptions: NavigateOptions = { replace: true }, +) { + const [searchParams, setSearchParams] = useSearchParams(); + const serializerRef = useRef(serialize); + const deserializerRef = useRef(deserialize); + + useEffect( + () => { + serializerRef.current = serialize; + }, + [serialize], + ); + + useEffect( + () => { + deserializerRef.current = deserialize; + }, + [deserialize], + ); + + const potentialValue = searchParams.get(key); + const value = useMemo( + // FIXME(frozenhelium): Reading ref.current in useMemo avoids stale closure + // eslint-disable-next-line react-hooks/refs + () => deserializerRef.current(potentialValue), + [potentialValue], + ); + + const setValue = useCallback( + (newValueOrGetNewValue: ValueOrSetter) => { + setSearchParams( + (prevParams) => { + const encodedValue = isCallable(newValueOrGetNewValue) + ? newValueOrGetNewValue(deserializerRef.current(prevParams.get(key))) + : newValueOrGetNewValue; + + const newValue = serializerRef.current(encodedValue); + if (isNotDefined(newValue)) { + prevParams.delete(key); + } else { + let serializedValue: string; + + if (typeof newValue === 'number') { + serializedValue = String(newValue); + } else if (typeof newValue === 'boolean') { + serializedValue = newValue ? 'true' : 'false'; + } else if (newValue instanceof Date) { + serializedValue = encodeDate(newValue); + } else { + serializedValue = newValue; + } + + prevParams.set(key, serializedValue); + } + + return prevParams; + }, + navigateOptions, + ); + }, + [setSearchParams, key, navigateOptions], + ); + + return [value, setValue] as const; +} + +export default useUrlSearchState; diff --git a/app/index.css b/app/index.css index e69de29..781eba8 100644 --- a/app/index.css +++ b/app/index.css @@ -0,0 +1,39 @@ + +::-webkit-scrollbar { + transform: translateX(100%); + background-color: var(--go-ui-color-scrollbar-background); + width: var(--go-ui-width-scrollbar); + height: var(--go-ui-width-scrollbar); +} + +::-webkit-scrollbar-track { + background-color: var(--go-ui-color-scrollbar-background); +} + +::-webkit-scrollbar-thumb { + border-radius: var(--go-ui-radius-scrollbar-border); + background-color: var(--go-ui-color-scrollbar-foreground); +} + +body { + line-height: var(--go-ui-line-height-md); + color: var(--go-ui-color-text); + font-family: var(--go-ui-font-family-sans-serif); + font-size: var(--go-ui-font-size-md); + font-weight: var(--go-ui-font-weight-normal); +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +p { + line-height: 1.5; +} + +a { + text-decoration: none; + color: inherit; +} \ No newline at end of file diff --git a/app/index.tsx b/app/index.tsx new file mode 100644 index 0000000..8319196 --- /dev/null +++ b/app/index.tsx @@ -0,0 +1,14 @@ +import '@ifrc-go/ui/index.css'; +import 'mapbox-gl/dist/mapbox-gl.css'; +import './index.css'; + +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; + +import App from './App.tsx'; + +createRoot(document.getElementById('webapp-root')!).render( + + + , +); diff --git a/app/main.tsx b/app/main.tsx deleted file mode 100644 index c04cc59..0000000 --- a/app/main.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import '@ifrc-go/ui/index.css'; -import './index.css'; - -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' - -import App from './App.tsx' - -createRoot(document.getElementById('root')!).render( - - - , -) diff --git a/app/resources/image/loginbackground.jpg b/app/resources/image/loginbackground.jpg new file mode 100644 index 0000000..79f9430 Binary files /dev/null and b/app/resources/image/loginbackground.jpg differ diff --git a/app/resources/image/logo.png b/app/resources/image/logo.png new file mode 100644 index 0000000..214f901 Binary files /dev/null and b/app/resources/image/logo.png differ diff --git a/app/resources/image/risk/cyclone.png b/app/resources/image/risk/cyclone.png new file mode 100644 index 0000000..858c956 Binary files /dev/null and b/app/resources/image/risk/cyclone.png differ diff --git a/app/resources/image/risk/drought.png b/app/resources/image/risk/drought.png new file mode 100644 index 0000000..dd2c3b3 Binary files /dev/null and b/app/resources/image/risk/drought.png differ diff --git a/app/resources/image/risk/earthquake.png b/app/resources/image/risk/earthquake.png new file mode 100644 index 0000000..8315870 Binary files /dev/null and b/app/resources/image/risk/earthquake.png differ diff --git a/app/resources/image/risk/flood.png b/app/resources/image/risk/flood.png new file mode 100644 index 0000000..0ec232a Binary files /dev/null and b/app/resources/image/risk/flood.png differ diff --git a/app/resources/image/risk/wildfire.png b/app/resources/image/risk/wildfire.png new file mode 100644 index 0000000..ac00a20 Binary files /dev/null and b/app/resources/image/risk/wildfire.png differ diff --git a/app/utils/constants.ts b/app/utils/constants.ts new file mode 100644 index 0000000..5a2d93f --- /dev/null +++ b/app/utils/constants.ts @@ -0,0 +1,85 @@ +import { listToMap } from '@togglecorp/fujs'; + +export const NUM_X_AXIS_TICKS_MIN = 3; +export const NUM_X_AXIS_TICKS_MAX = 12; + +export const DEFAULT_X_AXIS_HEIGHT = 26; +export const DEFAULT_Y_AXIS_WIDTH = 46; +export const DEFAULT_Y_AXIS_WIDTH_WITH_LABEL = 66; + +export const defaultChartMargin = { + top: 0, + right: 0, + bottom: 0, + left: 0, +}; + +export const defaultChartPadding = { + top: 10, + right: 10, + bottom: 10, + left: 10, +}; + +// Map +export const DURATION_MAP_ZOOM = 1000; +export const DEFAULT_MAP_PADDING = 50; + +// Storage + +export const KEY_USER_STORAGE = 'user'; +export const KEY_LANGUAGE_STORAGE = 'language'; + +// Search page + +export const KEY_URL_SEARCH = 'keyword'; +export const SEARCH_TEXT_LENGTH_MIN = 3; + +// Risk + +export const COLOR_HAZARD_CYCLONE = '#a4bede'; +export const COLOR_HAZARD_DROUGHT = '#b68fba'; +export const COLOR_HAZARD_FOOD_INSECURITY = '#b7c992'; +export const COLOR_HAZARD_FLOOD = '#5a80b0'; +export const COLOR_HAZARD_EARTHQUAKE = '#eca48c'; +export const COLOR_HAZARD_STORM = '#97b8c2'; +export const COLOR_HAZARD_WILDFIRE = '#ff5014'; + +// FIXME: should these constants satisfy an existing enum? +export const CATEGORY_RISK_VERY_LOW = 1; +export const CATEGORY_RISK_LOW = 2; +export const CATEGORY_RISK_MEDIUM = 3; +export const CATEGORY_RISK_HIGH = 4; +export const CATEGORY_RISK_VERY_HIGH = 5; + +// Colors + +export const COLOR_WHITE = '#ffffff'; +// export const COLOR_TEXT = '#313131'; +// export const COLOR_TEXT_ON_DARK = '#ffffff'; +export const COLOR_LIGHT_GREY = '#e0e0e0'; +export const COLOR_DARK_GREY = '#a5a5a5'; +export const COLOR_BLACK = '#000000'; +// const COLOR_LIGHT_YELLOW = '#ffd470'; +export const COLOR_YELLOW = '#ff9e00'; +export const COLOR_BLUE = '#4c5d9b'; +export const COLOR_LIGHT_BLUE = '#c7d3e0'; +export const COLOR_ORANGE = '#ff8000'; +export const COLOR_RED = '#f5333f'; +export const COLOR_GREEN = '#8BB656'; +// const COLOR_DARK_RED = '#730413'; +export const COLOR_PRIMARY_BLUE = '#011e41'; +export const COLOR_PRIMARY_RED = '#f5333f'; + +export const COLOR_ACTIVE_REGION = '#7d8b9d'; + +// Import template + +export const FONT_FAMILY_HEADER = 'Montserrat'; + +export const monthKeyList = Array.from(Array(12).keys()); +export const multiMonthSelectDefaultValue = listToMap( + monthKeyList, + (key) => key, + () => false, +); diff --git a/app/utils/geo.ts b/app/utils/geo.ts new file mode 100644 index 0000000..061b162 --- /dev/null +++ b/app/utils/geo.ts @@ -0,0 +1,17 @@ +import getBbox from '@turf/bbox'; + +/** + * Returns the bounding box [west, south, east, north] for any GeoJSON object. + * + * Accepts `Record` so that both proper GeoJSON types and API + * response data (typed as `{ [key: string]: unknown }`) can be passed without + * casting at call sites. The return type `[number, number, number, number]` is + * directly compatible with mapbox-gl's `LngLatBoundsLike`. + */ +// eslint-disable-next-line import/prefer-default-export +export function getGeoJsonBounds( + geojson: GeoJSON.GeoJSON | Record, +): [number, number, number, number] { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return getBbox(geojson as any) as [number, number, number, number]; +} diff --git a/app/utils/map.ts b/app/utils/map.ts new file mode 100644 index 0000000..ac6282b --- /dev/null +++ b/app/utils/map.ts @@ -0,0 +1,55 @@ +import { isDefined } from '@togglecorp/fujs'; +import getBbox from '@turf/bbox'; +import type { + Map, + NavigationControl, +} from 'mapbox-gl'; + +import type { GoApiResponse } from './restRequest'; + +export const defaultMapStyle = 'mapbox://styles/go-ifrc/ckrfe16ru4c8718phmckdfjh0'; +export const localUnitMapStyle = 'mapbox://styles/go-ifrc/clvvgugzh00x501pc1n00b8cz'; +type CountryResponse = GoApiResponse<'/api/v2/country/{id}/'> + +type NavControlOptions = NonNullable[0]>; +export const defaultNavControlOptions: NavControlOptions = { + showCompass: false, +}; + +type ControlPosition = NonNullable[1]>; +export const defaultNavControlPosition: ControlPosition = 'top-right'; + +export const defaultMapOptions: Omit = { + logoPosition: 'bottom-left' as const, + zoom: 1.5, + minZoom: 1, + maxZoom: 18, + scrollZoom: false, + pitchWithRotate: false, + dragRotate: false, + renderWorldCopies: true, + attributionControl: false, + preserveDrawingBuffer: true, + // interactive: false, +}; + +export function getCountryListBoundingBox(countryList:CountryResponse[]) { + if (countryList.length < 1) { + return undefined; + } + + const countryWithBbox = countryList.filter((country) => isDefined(country.bbox)); + + if (countryWithBbox.length < 1) { + return undefined; + } + const collection = { + type: 'FeatureCollection' as const, + features: countryWithBbox.map((country) => ({ + type: 'Feature' as const, + geometry: country.bbox, + })), + }; + + return getBbox(collection); +} diff --git a/app/utils/resolveUrl.ts b/app/utils/resolveUrl.ts new file mode 100644 index 0000000..8283994 --- /dev/null +++ b/app/utils/resolveUrl.ts @@ -0,0 +1,17 @@ +// eslint-disable-next-line import/prefer-default-export +export function resolveUrl(base: string, endpoint: string) { + // If endpoint is already an absolute URL, return it as-is + if (/^https?:\/\//i.test(endpoint)) { + return endpoint; + } + + // Ensure base ends with a slash + const normalizedBase = base.endsWith('/') ? base : `${base}/`; + + // Remove leading slash from endpoint to avoid URL() resetting the path + const normalizedEndpoint = endpoint.startsWith('/') + ? endpoint.slice(1) + : endpoint; + + return new URL(normalizedEndpoint, normalizedBase).toString(); +} diff --git a/app/utils/restRequest/error.ts b/app/utils/restRequest/error.ts new file mode 100644 index 0000000..07a309f --- /dev/null +++ b/app/utils/restRequest/error.ts @@ -0,0 +1,179 @@ +import { + isNotDefined, + listToMap, + mapToMap, +} from '@togglecorp/fujs'; +import { nonFieldError } from '@togglecorp/toggle-form'; + +// NOTE: Some leaf can also have string error +type ResponseLeafError = string | string[]; + +export type ResponseObjectError = { + [key: string]: ResponseObjectError | ResponseArrayError | ResponseLeafError | undefined; +} & { + non_field_errors?: string[] | undefined; + internal_non_field_errors?: string[] | undefined; +}; + +type ResponseArrayError = (ResponseObjectError | ResponseArrayError | ResponseLeafError)[]; + +type FormLeafError = string; +type FormArrayError = { + [key: string]: FormLeafError | FormObjectError | FormArrayError | undefined; +} +type FormObjectError = { + [nonFieldError]?: string +} & { + [key: string]: FormLeafError | FormObjectError | FormArrayError | undefined; +}; + +export const NUM = Symbol('NUMBER'); + +export function matchArray( + array: (string | number)[], + expressions: (string | number | typeof NUM)[], +) { + if (array.length !== expressions.length) { + return undefined; + } + const response: number[] = []; + for (let i = 0; i < array.length; i += 1) { + const item = array[i]; + const expression = expressions[i]; + + if (expression === NUM) { + if (typeof item !== 'number') { + return undefined; + } + + response.push(item); + // eslint-disable-next-line no-continue + continue; + } + + if (expression === item) { + // eslint-disable-next-line no-continue + continue; + } + + return undefined; + } + return response; +} + +function isObjectError( + err: ResponseObjectError | ResponseArrayError | ResponseLeafError, +): err is ResponseObjectError { + return !Array.isArray(err) && typeof err === 'object'; +} + +function isLeafError( + err: ResponseObjectError | ResponseArrayError | ResponseLeafError, +): err is ResponseLeafError { + if (isObjectError(err)) { + return false; + } + if (typeof err === 'string') { + return true; + } + const unsafeErr: unknown[] = err; + return Array.isArray(unsafeErr) && unsafeErr.every( + (e: unknown) => typeof e === 'string', + ); +} + +function isArrayError( + err: ResponseObjectError | ResponseArrayError | ResponseLeafError, +): err is ResponseLeafError { + if (isObjectError(err)) { + return false; + } + if (isLeafError(err)) { + return false; + } + return Array.isArray(err) && err.every( + (e) => isArrayError(e) || isObjectError(e), + ); +} + +function transformLeafError(error: ResponseLeafError | undefined) { + if (isNotDefined(error)) { + return undefined; + } + if (typeof error === 'string') { + return error; + } + return error.join(' '); +} + +type Location = (string | number)[]; +type GetKey = (location: Location) => string | number | undefined; + +function transformArrayError( + error: ResponseArrayError | undefined, + getKey: GetKey, + location: Location = [], +): FormArrayError | undefined { + if (isNotDefined(error)) { + return undefined; + } + const EMPTY_KEY = ''; + const formErrors = listToMap( + error, + (_, index) => getKey([...location, index]) ?? EMPTY_KEY, + (memberError, _, index) => { + if (isObjectError(memberError)) { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + return transformObjectError(memberError, getKey, [...location, index]); + } + if (isLeafError(memberError)) { + return transformLeafError(memberError); + } + if (isArrayError(memberError)) { + return transformArrayError(memberError, getKey, [...location, index]); + } + return undefined; + }, + ); + delete formErrors[EMPTY_KEY]; + return formErrors; +} + +export function transformObjectError( + error: ResponseObjectError | undefined, + getKey: GetKey, + location: Location = [], +): FormObjectError | undefined { + if (isNotDefined(error)) { + return undefined; + } + const { + non_field_errors, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + internal_non_field_errors, + ...fieldErrors + } = error; + + return { + [nonFieldError]: non_field_errors?.join(' '), + ...mapToMap( + fieldErrors, + (key) => key, + (fieldError, key) => { + if (isNotDefined(fieldError)) { + return undefined; + } + if (isObjectError(fieldError)) { + return transformObjectError(fieldError, getKey, [...location, key]); + } + if (isLeafError(fieldError)) { + return transformLeafError(fieldError); + } + if (isArrayError(fieldError)) { + return transformArrayError(fieldError, getKey, [...location, key]); + } + return undefined; + }, + ), + }; +} diff --git a/app/utils/restRequest/go.ts b/app/utils/restRequest/go.ts new file mode 100644 index 0000000..784b30b --- /dev/null +++ b/app/utils/restRequest/go.ts @@ -0,0 +1,383 @@ +import { type Language } from '@ifrc-go/ui/contexts'; +import { + isDefined, + isFalsyString, + isNotDefined, +} from '@togglecorp/fujs'; +import { type ContextInterface } from '@togglecorp/toggle-request'; + +import { + goApi, + riskApi, +} from '#config'; +import { resolveUrl } from '#utils/resolveUrl'; + +import { type ResponseObjectError } from './error'; + +const CONTENT_TYPE_JSON = 'application/json'; +const CONTENT_TYPE_CSV = 'text/csv'; +const CONTENT_TYPE_EXCEL = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; + +type ResponseError = { + status: number; + originalResponse: Response, + responseText: string; +} + +export interface TransformedError { + value: { + formErrors: ResponseObjectError, + messageForNotification: string, + }; + status: number | undefined; + debugMessage: string; +} + +type ApiType = 'go' | 'risk' | 'translation'; + +export interface AdditionalOptions { + apiType?: ApiType; + formData?: boolean; + isCsvRequest?: boolean; + enforceEnglishForQuery?: boolean; + // FIXME using the current language should be default behaviour so we might not need this. + useCurrentLanguageForMutation?: boolean; + enforceLanguageForMutation?: Language; + isExcelRequest?: boolean; +} + +function transformError( + response: ResponseError, + fallbackMessage: string, +): ResponseObjectError | string { + const { + originalResponse, + responseText, + } = response; + + if (originalResponse.status.toLocaleString()[0] === '5') { + return 'Internal server error!'; + } + + if (isFalsyString(responseText)) { + return 'Empty error response from server!'; + } + + if (originalResponse.headers.get('content-type') === CONTENT_TYPE_JSON) { + try { + const json = JSON.parse(responseText); + // Non Standard Error + if (typeof json !== 'object' || Array.isArray(json) || isNotDefined(json)) { + return fallbackMessage; + } + // Old Standard Error + if ( + typeof json.statusCode === 'number' + && typeof json.error_message === 'string' + ) { + return json.error_message; + } + // New Standard Error + if ( + typeof json.errors === 'object' + && !Array.isArray(json.errors) + ) { + return json.errors; + } + // Non Standard Error + return fallbackMessage; + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + return fallbackMessage; + } + } + + if (typeof responseText === 'string') { + return responseText; + } + + return fallbackMessage; +} + +type GoContextInterface = ContextInterface< + unknown, + ResponseError, + TransformedError, + AdditionalOptions +>; + +function getEndPoint(apiType: ApiType | undefined) { + if (apiType === 'risk') { + return riskApi; + } + + return goApi; +} + +export const processGoUrls: GoContextInterface['transformUrl'] = (url, _, additionalOptions) => { + if (isFalsyString(url)) { + return ''; + } + + // external URL + if (/^https?:\/\//i.test(url)) { + return url; + } + + const { apiType } = additionalOptions; + + const resolvedUrl = resolveUrl( + getEndPoint(apiType) ?? '', + url, + ); + + return resolvedUrl; +}; + +type Literal = string | number | boolean | File; +type FormDataCompatibleObj = Record; + +function getFormData(jsonData: FormDataCompatibleObj) { + const formData = new FormData(); + Object.keys(jsonData || {}).forEach( + (key) => { + const value = jsonData?.[key]; + if (value && Array.isArray(value)) { + value.forEach((v) => { + formData.append(key, v instanceof Blob ? v : String(v)); + }); + } else if (isDefined(value)) { + formData.append(key, value instanceof Blob ? value : String(value)); + } else { + formData.append(key, ''); + } + }, + ); + return formData; +} + +export const processGoOptions: GoContextInterface['transformOptions'] = ( + _, + requestOptions, + extraOptions, +) => { + const { + body, + headers, + method = 'GET', + ...otherOptions + } = requestOptions; + + const { + // FIXME: undefined type should only be applicable for the external requests + formData, + isCsvRequest, + isExcelRequest, + useCurrentLanguageForMutation = false, + enforceLanguageForMutation, + } = extraOptions; + + // const currentLanguage = getFromStorage(KEY_LANGUAGE_STORAGE) ?? 'en'; + // const user = getFromStorage(KEY_USER_STORAGE); + // const token = user?.token; + + // FIXME: only inject on go apis + const defaultHeaders: HeadersInit = {}; + + // if (apiType === 'go' && isDefined(token)) { + // defaultHeaders.Authorization = `Token ${token}`; + // } + + if (method === 'GET') { + // Query + defaultHeaders['Accept-Language'] = 'en'; + } else if (method === 'POST' || method === 'PUT' || method === 'PATCH') { + // Mutation + if (isDefined(enforceLanguageForMutation)) { + defaultHeaders['Accept-Language'] = enforceLanguageForMutation; + } else if (useCurrentLanguageForMutation) { + defaultHeaders['Accept-Language'] = 'en'; + } else { + defaultHeaders['Accept-Language'] = 'en'; + } + } else { + defaultHeaders['Accept-Language'] = 'en'; + } + + if (formData) { + const requestBody = getFormData(body as FormDataCompatibleObj); + return { + method, + headers: { + ...defaultHeaders, + ...headers, + }, + body: requestBody, + ...otherOptions, + }; + } + + const requestBody = body ? JSON.stringify(body) : undefined; + + let contentType: string = CONTENT_TYPE_JSON; + if (isCsvRequest) { + contentType = CONTENT_TYPE_CSV; + } else if (isExcelRequest) { + contentType = CONTENT_TYPE_EXCEL; + } + + const specificHeaders = { + Accept: contentType, + 'Content-Type': contentType, + }; + + return { + method, + headers: { + ...defaultHeaders, + ...specificHeaders, + ...headers, + }, + body: requestBody, + ...otherOptions, + }; +}; + +const isSuccessfulStatus = (status: number): boolean => status >= 200 && status < 300; + +const isContentTypeExcel = (res: Response): boolean => res.headers.get('content-type') === CONTENT_TYPE_EXCEL; + +const isContentTypeJson = (res: Response): boolean => { + const contentTypeHeaders = res.headers.get('content-type'); + + if (isNotDefined(contentTypeHeaders)) { + return false; + } + + const mediaTypes = contentTypeHeaders.split('; '); + + return mediaTypes[0]?.toLowerCase() === CONTENT_TYPE_JSON; +}; + +const isLoginRedirect = (url: string): boolean => new URL(url).pathname.includes('login'); + +export const processGoResponse: GoContextInterface['transformResponse'] = async ( + res, +) => { + if (res.redirected && isLoginRedirect(res.url)) { + throw new Error('Redirected by server'); + } + + if (isSuccessfulStatus(res.status)) { + if (isContentTypeExcel(res)) { + return res.blob(); + } + + const resText = await res.text(); + if (isContentTypeJson(res)) { + return JSON.parse(resText); + } + + return resText; + } + + const originalResponse = res.clone(); + const resText = await res.text(); + + return { + status: res.status, + originalResponse, + responseText: resText, + }; +}; + +export const processGoError: GoContextInterface['transformError'] = ( + responseError, + url, + requestOptions, +) => { + if (responseError === 'network') { + const err = { + non_field_errors: ['Network error'], + }; + return { + reason: 'network', + value: { + messageForNotification: 'Cannot connect with the server! Please, make sure you have an active internet connection and try again!', + formErrors: err, + }, + status: undefined, + debugMessage: JSON.stringify({ + url, + status: undefined, + requestOptions, + error: 'Network error', + }), + }; + } + + if (responseError === 'parse') { + const err = { + non_field_errors: ['Response parse error'], + }; + return { + reason: 'parse', + value: { + messageForNotification: 'There was a problem parsing the response from server', + formErrors: err, + }, + status: undefined, + debugMessage: JSON.stringify({ + url, + status: undefined, + requestOptions, + error: 'Response parse error', + }), + }; + } + + const { method } = requestOptions; + + // default fallback message for GET + let fallbackMessage = 'Failed to load data'; + + if (method !== 'GET') { + switch (responseError?.status) { + case 401: + fallbackMessage = 'You do not have enough permission to perform this action'; + break; + case 413: + fallbackMessage = 'Your request was refused because the payload was too large'; + break; + default: + fallbackMessage = 'Some error occurred while performing this action.'; + break; + } + } + + const formErrors = transformError(responseError, fallbackMessage); + + return { + reason: 'server', + value: { + formErrors: typeof formErrors === 'string' + ? { non_field_errors: [formErrors] } + : formErrors, + messageForNotification: typeof formErrors === 'string' + ? formErrors + : formErrors.non_field_errors?.join(' ') || fallbackMessage, + // errorText: responseError.responseText, + }, + status: responseError?.status, + + // FIXME: We should not stringify the whole response eagerly + debugMessage: JSON.stringify({ + url, + status: responseError?.status, + requestOptions, + error: 'Request rejected by the server', + responseText: responseError.responseText, + }), + }; +}; diff --git a/app/utils/restRequest/index.ts b/app/utils/restRequest/index.ts new file mode 100644 index 0000000..65ce0b3 --- /dev/null +++ b/app/utils/restRequest/index.ts @@ -0,0 +1,78 @@ +import { + RequestContext, + useLazyRequest, + useRequest, +} from '@togglecorp/toggle-request'; + +import type { paths as riskApiPaths } from '#generated/riskTypes'; +import type { paths as goApiPaths } from '#generated/types'; + +import type { + ApiBody, + ApiResponse, + ApiUrlQuery, + CustomLazyRequestOptions, + CustomLazyRequestReturn, + CustomRequestOptions, + CustomRequestReturn, + ExternalRequestOptions, + ExternalRequestReturn, + VALID_METHOD, +} from './overrideTypes'; + +export type GoApiResponse = ApiResponse; +export type GoApiUrlQuery = ApiUrlQuery +export type GoApiBody = ApiBody + +export type RiskApiResponse = ApiResponse; + +export type ListResponseItem +} | undefined> = NonNullable['results']>[number]; + +// FIXME: identify a way to do this without a cast +const useGoRequest = useRequest as < + PATH extends keyof goApiPaths, + METHOD extends VALID_METHOD | undefined = 'GET', +>( + requestOptions: CustomRequestOptions +) => CustomRequestReturn; + +// FIXME: identify a way to do this without a cast +const useGoLazyRequest = useLazyRequest as < + PATH extends keyof goApiPaths, + CONTEXT = unknown, + METHOD extends VALID_METHOD | undefined = 'GET', +>( + requestOptions: CustomLazyRequestOptions +) => CustomLazyRequestReturn; + +// FIXME: identify a way to do this without a cast +const useRiskRequest = useRequest as < + PATH extends keyof riskApiPaths, + METHOD extends VALID_METHOD | undefined = 'GET', +>( + requestOptions: CustomRequestOptions & { apiType: 'risk' }, +) => CustomRequestReturn; + +// FIXME: identify a way to do this without a cast +const useRiskLazyRequest = useLazyRequest as < + PATH extends keyof riskApiPaths, + CONTEXT = unknown, + METHOD extends VALID_METHOD | undefined = 'GET', +>( + requestOptions: CustomLazyRequestOptions & { apiType: 'risk' } +) => CustomLazyRequestReturn; + +const useExternalRequest = useRequest as ( + requestOptions: ExternalRequestOptions, +) => ExternalRequestReturn; + +export { + RequestContext, + useExternalRequest, + useGoLazyRequest as useLazyRequest, + useGoRequest as useRequest, + useRiskLazyRequest, + useRiskRequest, +}; diff --git a/app/utils/restRequest/overrideTypes.ts b/app/utils/restRequest/overrideTypes.ts new file mode 100644 index 0000000..9911d6d --- /dev/null +++ b/app/utils/restRequest/overrideTypes.ts @@ -0,0 +1,372 @@ +import { type DeepNevaRemove } from '@ifrc-go/ui/utils'; +import { + type LazyRequestOptions, + type RequestOptions, + type useLazyRequest, + type useRequest, +} from '@togglecorp/toggle-request'; + +import { + type AdditionalOptions, + type TransformedError, +} from './go'; + +export type VALID_METHOD = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; + +// Helper Type Resolvers + +export type ApiResponse = ( + METHOD extends 'GET' + ? SCHEME[URL] extends { get: { responses: { 200: { content: { 'application/json': infer Res } } } } } + ? DeepNevaRemove + : never + : METHOD extends 'POST' + ? SCHEME[URL] extends { post: { responses: { 201: { content: { 'application/json': infer Res } } } } } + ? DeepNevaRemove + : never + : METHOD extends 'PATCH' + ? SCHEME[URL] extends { patch: { responses: { 200: { content: { 'application/json': infer Res } } } } } + ? DeepNevaRemove + : never + : METHOD extends 'PUT' + ? SCHEME[URL] extends { put: { responses: { 200: { content: { 'application/json': infer Res } } } } } + ? DeepNevaRemove + : never + : never +) + +export type ApiUrlQuery = ( + METHOD extends 'GET' + ? SCHEME[URL] extends { get: { parameters: { query?: infer Query } } } + ? Query + : never + : METHOD extends 'POST' + ? SCHEME[URL] extends { post: { parameters: { query?: infer Query } } } + ? Query + : never + : METHOD extends 'PATCH' + ? SCHEME[URL] extends { patch: { parameters: { query?: infer Query } } } + ? Query + : never + : METHOD extends 'PUT' + ? SCHEME[URL] extends { put: { parameters: { query?: infer Query } } } + ? Query + : never + : METHOD extends 'DELETE' + ? SCHEME[URL] extends { delete: { parameters: { query?: infer Query } } } + ? Query + : never + : never +); + +export type ApiBody = ( + METHOD extends 'POST' + ? SCHEME[URL] extends { post: { requestBody?: { content: { 'application/json': infer Res } } } } + ? DeepNevaRemove + : never + : METHOD extends 'PATCH' + ? SCHEME[URL] extends { patch: { requestBody?: { content: { 'application/json': infer Res } } } } + ? DeepNevaRemove + : never + : METHOD extends 'PUT' + ? SCHEME[URL] extends { put: { requestBody?: { content: { 'application/json': infer Res } } } } + ? DeepNevaRemove + : never + : never +) +// Type Resolvers + +// NOTE: If the method is POST, the server generates typings for 201 +// Also, adding a fallback so that we still support 200 +type ResolveResponseContent = ( + METHOD extends 'POST' + ? RESPONSES extends { 201 : { content: { 'application/json': infer Response } } } + ? DeepNevaRemove + : ResolveResponseContent + : RESPONSES extends { 200 : { content: { 'application/json': infer Response } } } + ? DeepNevaRemove + : unknown +); + +type ResolvePath = ( + PARAMETERS extends { path: infer Path } + ? Path + : unknown +); + +type ResolveQuery = ( + PARAMETERS extends { query?: infer Query } + ? Query + : unknown +); + +type ResolveRequestBody = ( + REQUEST_BODY extends { content: { 'application/json': infer RequestBody } } + ? DeepNevaRemove + : unknown +); + +type GetResponse = ( + SCHEMA[PATH] extends { get: { responses: infer Responses } } + ? ResolveResponseContent + : unknown +); +type PutResponse = ( + SCHEMA[PATH] extends { put: { responses: infer Responses } } + ? ResolveResponseContent + : unknown +); +type PostResponse = ( + SCHEMA[PATH] extends { post: { responses: infer Responses } } + ? ResolveResponseContent + : unknown +); +type PatchResponse = ( + SCHEMA[PATH] extends { patch: { responses: infer Responses } } + ? ResolveResponseContent + : unknown +); + +// Options + +type CommonOptions = { + pathVariables?: ResolvePath + | ((context: CONTEXT) => ResolvePath | undefined), + + // query?: ResolveQuery, + mockResponse?: ResolveResponseContent, + shouldRetry?: ( + // eslint-disable-next-line max-len + val: { errored: true, value: TransformedError } | { errored: false, value: ResolveResponseContent }, + run: number, + context: CONTEXT, + ) => number; + shouldPoll?: ( + // eslint-disable-next-line max-len + val: { errored: true, value: TransformedError } | { errored: false, value: ResolveResponseContent }, + context: CONTEXT + ) => number; + onSuccess?: (val: ResolveResponseContent, context: CONTEXT) => void; + + onFailure?: (val: TransformedError, context: CONTEXT) => void; +} + +type GetOptions = ( + SCHEMA[PATH] extends { + get: { + parameters: infer Parameters, + responses: infer Responses, + }, + } ? ({ + url: PATH, + method?: 'GET', + // FIXME: This should only be for lazy + query?: ResolveQuery + | ((context: CONTEXT) => ResolveQuery | undefined), + } & CommonOptions<'GET', Parameters, Responses, CONTEXT>) : unknown +); + +type PutOptions = ( + SCHEMA[PATH] extends { + put: { + parameters: infer Parameters, + responses: infer Responses, + requestBody?: infer RequestBody, + }, + } ? ({ + url: PATH, + method: 'PUT', + body: ResolveRequestBody + | ((context: CONTEXT) => ResolveRequestBody), + other?: Omit + | ((context: CONTEXT) => Omit | undefined), + query?: ResolveQuery + | ((context: CONTEXT) => ResolveQuery | undefined), + } & CommonOptions<'PUT', Parameters, Responses, CONTEXT>) : unknown +); + +type PatchOptions = ( + SCHEMA[PATH] extends { + patch: { + parameters: infer Parameters, + responses: infer Responses, + requestBody?: infer RequestBody, + }, + } ? ({ + url: PATH, + method: 'PATCH', + body: ResolveRequestBody + | ((context: CONTEXT) => ResolveRequestBody), + other?: Omit + | ((context: CONTEXT) => Omit | undefined), + query?: ResolveQuery + | ((context: CONTEXT) => ResolveQuery | undefined), + } & CommonOptions<'PATCH', Parameters, Responses, CONTEXT>) : unknown +); + +type PostOptions = ( + SCHEMA[PATH] extends { + post: { + parameters: infer Parameters, + responses: infer Responses, + requestBody?: infer RequestBody, + }, + } ? ({ + url: PATH, + method: 'POST', + body: ResolveRequestBody + | ((context: CONTEXT) => ResolveRequestBody), + other?: Omit + | ((context: CONTEXT) => Omit | undefined), + query?: ResolveQuery + | ((context: CONTEXT) => ResolveQuery | undefined), + } & CommonOptions<'POST', Parameters, Responses, CONTEXT>) : unknown +); + +type DeleteOptions = ( + SCHEMA[PATH] extends { + delete: { + parameters: infer Parameters, + responses: infer Responses, + } + } ? ({ + url: PATH, + method: 'DELETE' + } & CommonOptions<'DELETE', Parameters, Responses, CONTEXT>) : unknown +); + +type OptionOmissions = ( + /* manually */ + 'url' + | 'method' + + /* common options */ + | 'query' + | 'mockResponse' + | 'shouldRetry' + | 'shouldPoll' + | 'onSuccess' + | 'onFailure' + + /* xxx options */ + | 'body' + | 'other' + | 'pathVariables' +); + +type RequestOptionsBase = { + url: PATH, + method?: METHOD, +} & Omit< + RequestOptions, + OptionOmissions +>; + +type LazyRequestOptionsBase = { + url: PATH, + method?: METHOD, +} & Omit< + LazyRequestOptions, + OptionOmissions +>; + +export type ExternalRequestOptions = Pick< +RequestOptions, +'query' | 'url' | 'skip' +>; + +export type CustomRequestOptions< + SCHEMA extends object, + PATH extends keyof SCHEMA, + METHOD extends VALID_METHOD | undefined, +> = ( + METHOD extends 'GET' | undefined + ? RequestOptionsBase & GetOptions + : METHOD extends 'PUT' + ? RequestOptionsBase & PutOptions + : METHOD extends 'PATCH' + ? RequestOptionsBase & PatchOptions + : METHOD extends 'POST' + ? RequestOptionsBase & PostOptions + : METHOD extends 'DELETE' + ? RequestOptionsBase & DeleteOptions + : never +); + +export type CustomLazyRequestOptions< + SCHEMA, + PATH extends keyof SCHEMA, + METHOD extends 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | undefined, + CONTEXT, +> = ( + METHOD extends 'GET' | undefined + ? LazyRequestOptionsBase + & GetOptions + : METHOD extends 'PUT' + ? LazyRequestOptionsBase + & PutOptions + : METHOD extends 'PATCH' + ? LazyRequestOptionsBase + & PatchOptions + : METHOD extends 'POST' + ? LazyRequestOptionsBase + & PostOptions + : METHOD extends 'DELETE' + ? LazyRequestOptionsBase + & DeleteOptions + : never +); + +// Return + +type RequestReturn = ReturnType>; + +type LazyRequestReturn = ReturnType>; + +export type ExternalRequestReturn = RequestReturn; + +export type CustomRequestReturn< + SCHEMA, + PATH extends keyof SCHEMA, + METHOD extends VALID_METHOD | undefined, +> = ( + METHOD extends 'GET' | undefined + ? RequestReturn> + : METHOD extends 'PUT' + ? RequestReturn> + : METHOD extends 'PATCH' + ? RequestReturn> + : METHOD extends 'POST' + ? RequestReturn> + : METHOD extends 'DELETE' + ? RequestReturn + : never +); + +export type CustomLazyRequestReturn< + SCHEMA, + PATH extends keyof SCHEMA, + METHOD extends 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | undefined, + CONTEXT, +> = ( + METHOD extends 'GET' | undefined + ? LazyRequestReturn, CONTEXT> + : METHOD extends 'PUT' + ? LazyRequestReturn, CONTEXT> + : METHOD extends 'PATCH' + ? LazyRequestReturn, CONTEXT> + : METHOD extends 'POST' + ? LazyRequestReturn, CONTEXT> + : METHOD extends 'DELETE' + ? LazyRequestReturn + : never +); diff --git a/app/utils/risk.ts b/app/utils/risk.ts new file mode 100644 index 0000000..1982104 --- /dev/null +++ b/app/utils/risk.ts @@ -0,0 +1,501 @@ +import { + avgSafe, + maxSafe, + sumSafe, +} from '@ifrc-go/ui/utils'; +import { + compareNumber, + isDefined, + isFalsyString, + isNotDefined, + listToGroupList, + mapToList, + mapToMap, + unique, +} from '@togglecorp/fujs'; + +import { type components } from '#generated/riskTypes'; +import { + CATEGORY_RISK_HIGH, + CATEGORY_RISK_LOW, + CATEGORY_RISK_MEDIUM, + CATEGORY_RISK_VERY_HIGH, + CATEGORY_RISK_VERY_LOW, + COLOR_HAZARD_CYCLONE, + COLOR_HAZARD_DROUGHT, + COLOR_HAZARD_EARTHQUAKE, + COLOR_HAZARD_FLOOD, + COLOR_HAZARD_FOOD_INSECURITY, + COLOR_HAZARD_STORM, + COLOR_HAZARD_WILDFIRE, + COLOR_LIGHT_GREY, +} from '#utils/constants'; + +import type { RiskApiResponse } from './restRequest'; + +export type HazardType = components['schemas']['CommonHazardTypeEnumKey']; +type IpcEstimationType = components['schemas']['EstimationTypeEnum']; +type CountrySeasonal = RiskApiResponse<'/api/v1/country-seasonal/'>; +type IpcData = CountrySeasonal[number]['ipc_displacement_data']; +type GwisData = CountrySeasonal[number]['gwis']; + +export const hazardTypeToColorMap: Record = { + EQ: COLOR_HAZARD_EARTHQUAKE, + FL: COLOR_HAZARD_FLOOD, + TC: COLOR_HAZARD_CYCLONE, + EP: COLOR_LIGHT_GREY, + FI: COLOR_HAZARD_FOOD_INSECURITY, + SS: COLOR_HAZARD_STORM, + DR: COLOR_HAZARD_DROUGHT, + TS: COLOR_HAZARD_CYCLONE, + CD: COLOR_LIGHT_GREY, + WF: COLOR_HAZARD_WILDFIRE, +}; + +export function getDataWithTruthyHazardType< + HAZARD_TYPE extends HazardType, + DATA extends { hazard_type?: '' | HAZARD_TYPE | undefined | null } +>(data: DATA) { + if (isFalsyString(data.hazard_type)) { + return undefined; + } + + return { + ...data, + // FIXME: server should not pass empty string + hazard_type: data.hazard_type as Exclude, + }; +} + +export interface RiskDataItem { + january?: number | null, + february?: number | null, + march?: number | null, + april?: number | null, + may?: number | null, + june?: number | null, + july?: number | null, + august?: number | null, + september?: number | null, + october?: number | null, + november?: number | null, + december?: number | null, + annual_average?: number | null, +} + +const monthToKeyMap: Record = { + 0: 'january', + 1: 'february', + 2: 'march', + 3: 'april', + 4: 'may', + 5: 'june', + 6: 'july', + 7: 'august', + 8: 'september', + 9: 'october', + 10: 'november', + 11: 'december', +}; + +export const monthNumberToNameMap: Record = { + ...monthToKeyMap, + // FIXME: we should not have these different + // class of data into same list + 12: 'annual_average', +}; + +export function getValueForSelectedMonths( + selectedMonths: Record | undefined, + riskDataItem: RiskDataItem | undefined, + aggregationMode: 'sum' | 'max' = 'sum', +) { + let annualValue; + + if (aggregationMode === 'sum') { + annualValue = sumSafe([ + riskDataItem?.january, + riskDataItem?.february, + riskDataItem?.march, + riskDataItem?.april, + riskDataItem?.may, + riskDataItem?.june, + riskDataItem?.july, + riskDataItem?.august, + riskDataItem?.september, + riskDataItem?.october, + riskDataItem?.november, + riskDataItem?.december, + ]); + } else if (aggregationMode === 'max') { + annualValue = maxSafe([ + riskDataItem?.january, + riskDataItem?.february, + riskDataItem?.march, + riskDataItem?.april, + riskDataItem?.may, + riskDataItem?.june, + riskDataItem?.july, + riskDataItem?.august, + riskDataItem?.september, + riskDataItem?.october, + riskDataItem?.november, + riskDataItem?.december, + ]); + } + + if (isNotDefined(selectedMonths) || selectedMonths[12] === true) { + return riskDataItem?.annual_average ?? annualValue ?? undefined; + } + + const monthKeys = Object.keys( + monthNumberToNameMap, + ) as unknown as (keyof typeof monthNumberToNameMap)[]; + + const valueList = monthKeys.map( + (key) => ( + selectedMonths[key] + ? riskDataItem?.[monthNumberToNameMap[key]!] + : undefined + ), + ); + + if (aggregationMode === 'max') { + return maxSafe(valueList); + } + + return sumSafe(valueList); +} + +const estimationPriorityMap: Record = { + current: 0, + first_projection: 1, + second_projection: 2, +}; + +export function getPrioritizedIpcData(data: IpcData) { + // For IPC, we can have multiple estimations or observed value + // for same year and month. + // So we need to prioritize entries that is from latest + // observation or prediction + // So the priority order is + // Observed > Estimated + // Latest analysis date + + // We sort the data by year, month first and then priority + // so that it's easier to extract the unique values + // NOTE: unique will keep first entry as unique and discard + // duplicate, so we need to sort by highest priority first + const sortedData = data?.map( + (item) => { + // FIXME: Update isFalsyString to Exclude empty string + // FIXME: Also fix this in server + if (isFalsyString(item.estimation_type)) { + return undefined; + } + + if (isFalsyString(item.analysis_date) || isNotDefined(item.total_displacement)) { + return undefined; + } + + return { + ...item, + estimation_type: item.estimation_type, + analysis_date: item.analysis_date, + estimation_priority: estimationPriorityMap[item.estimation_type], + analysisTimestamp: new Date(item.analysis_date).getTime(), + total_displacement: item.total_displacement, + }; + }, + ).filter(isDefined).sort((a, b) => ( + compareNumber(a.year, b.year) + || compareNumber(a.month, b.month) + || compareNumber(a.estimation_priority, b.estimation_priority) + || compareNumber(a.analysisTimestamp, b.analysisTimestamp, -1) + )) ?? []; + + const uniqueData = unique( + sortedData, + (ipcDataItem) => `${ipcDataItem.year}-${ipcDataItem.month}`, + ); + + return uniqueData; +} + +function getAverageIpcData(uniqueData: IpcData) { + const monthGroupedIpcData = listToGroupList( + uniqueData, + (datum) => datum.month, + (datum) => datum.total_displacement, + ); + + const ipcRiskDataItem = mapToMap( + monthGroupedIpcData, + (key) => monthNumberToNameMap[Number(key) - 1]!, + (item) => avgSafe(item), + ); + + const monthlyValueList = Object.values(ipcRiskDataItem ?? {}); + const annual_average = maxSafe(monthlyValueList); + + return { + ...ipcRiskDataItem, + annual_average, + }; +} + +export function getFiRiskDataItem(data: IpcData | undefined) { + if (isNotDefined(data) || data.length === 0) { + return undefined; + } + + const uniqueData = getPrioritizedIpcData(data); + const ipcRiskDataItem = getAverageIpcData(uniqueData); + + // To get additional information (i.e. hazard type display, country details + // which is common to all entry + const firstData = data[0]; + + return { + hazard_type: 'FI' as const, + hazard_type_display: firstData?.hazard_type_display, + country_details: firstData?.country_details, + ...ipcRiskDataItem, + }; +} + +export function getWfRiskDataItem(data: GwisData | undefined) { + if (isNotDefined(data) || data.length === 0) { + return undefined; + } + + const uniqueData = unique( + data, + (gwisDataItem) => `${gwisDataItem.year}-${gwisDataItem.month}`, + ); + + const monthGroupedGwisData = listToGroupList( + uniqueData, + (datum) => datum.month, + (datum) => datum.dsr_avg, + ); + + const gwisRiskDataItem = mapToMap( + monthGroupedGwisData, + (monthKey) => monthNumberToNameMap[Number(monthKey) - 1]!, + (item) => avgSafe(item), + ); + + const monthlyValueList = Object.values(gwisRiskDataItem); + const annual_average = avgSafe(monthlyValueList); + + // To get additional information (i.e. hazard type display, country details + // which is common to all entry + const firstYearData = data[0]; + return { + hazard_type: 'WF' as const, + hazard_type_display: firstYearData?.hazard_type_display, + country_details: firstYearData?.country_details, + ...gwisRiskDataItem, + annual_average, + }; +} + +export function hasSomeDefinedValue(riskDataItem: RiskDataItem) { + return mapToList( + monthNumberToNameMap, + // FIXME: we should avoid !== 0 condition + (value) => isDefined(riskDataItem[value]) && riskDataItem[value] !== 0, + ).some(Boolean); +} + +type RiskCategory = 1 | 2 | 3 | 4 | 5; + +export function riskScoreToCategory( + score: number | undefined | null, + hazardType: HazardType, +) { + if (isNotDefined(score) || score < 0) { + return undefined; + } + + // Wildfire categorization loosely based on + // https://link.springer.com/article/10.1007/s11069-021-05054-4/tables/2 + // + // Inform risk categorization based on + // https://drmkc.jrc.ec.europa.eu/inform-index/INFORM-Risk/Results-and-data/moduleId/1782/id/453/controller/Admin/action/Results#inline-nav-2 + const scoreCategories = hazardType === 'WF' ? [ + { score: 1000, category: CATEGORY_RISK_VERY_HIGH }, + { score: 17, category: CATEGORY_RISK_HIGH }, + { score: 9, category: CATEGORY_RISK_MEDIUM }, + { score: 5, category: CATEGORY_RISK_LOW }, + { score: 2, category: CATEGORY_RISK_VERY_LOW }, + ] : [ + { score: 10, category: CATEGORY_RISK_VERY_HIGH }, + { score: 6.5, category: CATEGORY_RISK_HIGH }, + { score: 5, category: CATEGORY_RISK_MEDIUM }, + { score: 3.5, category: CATEGORY_RISK_LOW }, + { score: 2, category: CATEGORY_RISK_VERY_LOW }, + ]; + + // Find category by comparing score to set of categories + // We start from the smallest first so we reverse the list + const currentCategory = scoreCategories.reverse().find( + (category) => score <= category.score, + ); + + if (isNotDefined(currentCategory)) { + return undefined; + } + + return currentCategory.category as RiskCategory; +} + +// TODO: implement full validation +export function isValidFeature( + maybeFeature: unknown, +): maybeFeature is GeoJSON.Feature { + if (typeof maybeFeature !== 'object') { + return false; + } + + if (isNotDefined(maybeFeature)) { + return false; + } + + if ( + !('type' in maybeFeature) + || !('geometry' in maybeFeature) + || !('properties' in maybeFeature) + ) { + return false; + } + + if (maybeFeature.type !== 'Feature' as const) { + return false; + } + + const safeObj = maybeFeature as GeoJSON.Feature; + if (safeObj.type !== 'Feature') { + return false; + } + + return true; +} + +export function isValidFeatureCollection( + maybeFeatureCollection: unknown, +): maybeFeatureCollection is GeoJSON.FeatureCollection { + if (isNotDefined(maybeFeatureCollection)) { + return false; + } + + if (typeof maybeFeatureCollection !== 'object') { + return false; + } + + const safeObj = maybeFeatureCollection as GeoJSON.FeatureCollection; + if (safeObj.type !== 'FeatureCollection') { + return false; + } + + if (!Array.isArray(safeObj.features)) { + return false; + } + + // TODO: validate each feature? + return true; +} + +// TODO: implement full validation +export function isValidPointFeature( + maybePointFeature: unknown, +): maybePointFeature is GeoJSON.Feature { + if (!isValidFeature(maybePointFeature)) { + return false; + } + + const safeObj = maybePointFeature as GeoJSON.Feature; + if (safeObj.geometry && safeObj.geometry.type !== 'Point') { + return false; + } + + return true; +} + +export interface HazardTypeOption { + hazard_type: HazardType; + hazard_type_display: string; +} + +export type RiskMetric = 'exposure' | 'displacement' | 'riskScore'; +export type RiskMetricOption = { + key: RiskMetric, + label: string; + applicableHazards: HazardType[]; +} + +export function riskMetricKeySelector(option: RiskMetricOption) { + return option.key; +} + +export function hazardTypeKeySelector(option: HazardTypeOption) { + return option.hazard_type; +} +export function hazardTypeLabelSelector(option: HazardTypeOption) { + return option.hazard_type_display; +} + +export const applicableHazardsByRiskMetric: Record = { + exposure: ['TC', 'FL', 'FI'], + displacement: ['TC', 'FL', 'SS'], + riskScore: ['DR', 'TC', 'FL', 'WF'], +}; + +export function getExposureRiskCategory(exposure: number) { + // Ten million + if (exposure > 10000000) { + return CATEGORY_RISK_VERY_HIGH; + } + + // One million + if (exposure > 1000000) { + return CATEGORY_RISK_HIGH; + } + + // Hundred thousand + if (exposure > 100000) { + return CATEGORY_RISK_MEDIUM; + } + + // Ten thousand + if (exposure > 10000) { + return CATEGORY_RISK_LOW; + } + + return CATEGORY_RISK_VERY_LOW; +} + +export function getDisplacementRiskCategory(displacement: number) { + // One million + if (displacement > 1000000) { + return CATEGORY_RISK_VERY_HIGH; + } + + // Hundred thousand + if (displacement > 100000) { + return CATEGORY_RISK_HIGH; + } + + // Ten thousand + if (displacement > 10000) { + return CATEGORY_RISK_MEDIUM; + } + + // One thousand + if (displacement > 1000) { + return CATEGORY_RISK_LOW; + } + + return CATEGORY_RISK_VERY_LOW; +} diff --git a/app/utils/tableHelpers/index.ts b/app/utils/tableHelpers/index.ts new file mode 100644 index 0000000..e5b8d51 --- /dev/null +++ b/app/utils/tableHelpers/index.ts @@ -0,0 +1,179 @@ +import { + type Column, + HeaderCell, + type HeaderCellProps, + type NumberOutputProps, + type SortDirection, +} from '@ifrc-go/ui'; +import { + createNumberColumn, + createStringColumn, +} from '@ifrc-go/ui/utils'; +import { _cs } from '@togglecorp/fujs'; + +import Link, { type Props as LinkProps } from '#components/Link'; + +import styles from './styles.module.css'; + +type Options = { + sortable?: boolean, + defaultSortDirection?: SortDirection, + + columnClassName?: string; + headerCellRendererClassName?: string; + headerContainerClassName?: string; + cellRendererClassName?: string; + cellContainerClassName?: string; + columnWidth?: Column['columnWidth']; + columnStretch?: Column['columnStretch']; + columnStyle?: Column['columnStyle']; + + headerInfoTitle?: HeaderCellProps['infoTitle']; + headerInfoDescription?: HeaderCellProps['infoDescription']; +} + +export function createLinkColumn( + id: string, + title: string, + accessor: (item: D) => React.ReactNode, + rendererParams: (item: D) => LinkProps, + options?: Options, +) { + const item: Column & { + valueSelector: (item: D) => string | undefined | null, + valueComparator: (foo: D, bar: D) => number, + } = { + id, + title, + headerCellRenderer: HeaderCell, + headerCellRendererParams: { + sortable: options?.sortable, + infoTitle: options?.headerInfoTitle, + infoDescription: options?.headerInfoDescription, + }, + cellRenderer: Link, + cellRendererParams: (_: K, datum: D): LinkProps => ({ + children: accessor(datum) || '--', + withUnderline: true, + ...rendererParams(datum), + }), + valueSelector: () => '', + valueComparator: () => 0, + cellRendererClassName: options?.cellRendererClassName, + columnClassName: options?.columnClassName, + headerCellRendererClassName: options?.headerCellRendererClassName, + cellContainerClassName: options?.cellContainerClassName, + columnWidth: options?.columnWidth, + columnStretch: options?.columnStretch, + columnStyle: options?.columnStyle, + }; + + return item; +} + +export function createCountryColumn( + id: string, + title: string, + accessor: (item: D) => React.ReactNode, + rendererParams: (item: D) => LinkProps, + options?: Options, +) { + return createLinkColumn( + id, + title, + accessor, + rendererParams, + { + ...options, + columnClassName: _cs(styles.country, options?.columnClassName), + }, + ); +} + +export function createEventColumn( + id: string, + title: string, + accessor: (item: D) => React.ReactNode, + rendererParams: (item: D) => LinkProps, + options?: Options, +) { + return createLinkColumn( + id, + title, + accessor, + rendererParams, + { + ...options, + columnClassName: _cs(styles.event, options?.columnClassName), + }, + ); +} + +export function createDisasterTypeColumn( + id: string, + title: string, + accessor: (item: D) => string | undefined | null, + options?: Options, +) { + return createStringColumn( + id, + title, + accessor, + { + ...options, + columnClassName: _cs(styles.disasterType, options?.columnClassName), + }, + ); +} + +export function createTitleColumn( + id: string, + title: string, + accessor: (item: D) => string | undefined | null, + options?: Options, +) { + return createStringColumn( + id, + title, + accessor, + { + ...options, + columnClassName: _cs(styles.title, options?.columnClassName), + }, + ); +} + +export function createAppealCodeColumn( + id: string, + title: string, + accessor: (item: D) => string | undefined | null, + options?: Options, +) { + return createStringColumn( + id, + title, + accessor, + { + ...options, + columnClassName: _cs(styles.appealCode, options?.columnClassName), + }, + ); +} + +export function createBudgetColumn( + id: string, + title: string, + accessor: (item: D) => number | undefined | null, + options?: Options, +) { + return createNumberColumn( + id, + title, + accessor, + { + suffix: ' CHF', + ...options, + columnClassName: _cs(styles.budget, options?.columnClassName), + }, + ); +} diff --git a/app/utils/tableHelpers/styles.module.css b/app/utils/tableHelpers/styles.module.css new file mode 100644 index 0000000..d7df63f --- /dev/null +++ b/app/utils/tableHelpers/styles.module.css @@ -0,0 +1,31 @@ +.region-list { + min-width: 8rem; +} + +.country-list { + min-width: 8rem; +} + +.country { + min-width: 8rem; +} + +.event { + min-width: 14rem; +} + +.title { + min-width: 14rem; +} + +.appeal-code { + min-width: 8rem; +} + +.disaster-type { + min-width: 8rem; +} + +.budget { + min-width: 8rem; +} diff --git a/app/utils/utils.ts b/app/utils/utils.ts new file mode 100644 index 0000000..7a45fd3 --- /dev/null +++ b/app/utils/utils.ts @@ -0,0 +1,141 @@ +import type { + CircleLayer, + CirclePaint, +} from 'mapbox-gl'; + +import type { AdminZeroFeatureProperties } from '#components/GlobalMap'; +import { + COLOR_BLACK, + COLOR_BLUE, + COLOR_ORANGE, + COLOR_RED, + COLOR_YELLOW, +} from '#utils/constants'; +import type { GoApiResponse } from '#utils/restRequest'; + +type GlobalEnumsResponse = GoApiResponse<'/api/v2/global-enums/'>; +type AppealTypeOption = NonNullable[number]; + +export const COLOR_EMERGENCY_APPEAL = COLOR_RED; +export const COLOR_DREF = COLOR_YELLOW; +export const COLOR_EAP = COLOR_BLUE; +export const COLOR_MULTIPLE_TYPES = COLOR_ORANGE; + +// FIXME: these must be a constant defined somewhere else +export const APPEAL_TYPE_DREF = 0; +export const APPEAL_TYPE_EMERGENCY = 1; +// const APPEAL_TYPE_INTERNATIONAL = 2; // TODO: we are not showing this? +export const APPEAL_TYPE_EAP = 3; +export const APPEAL_TYPE_MULTIPLE = -1; + +const circleColor: CirclePaint['circle-color'] = [ + 'match', + ['get', 'appealType'], + APPEAL_TYPE_DREF, + + COLOR_DREF, + APPEAL_TYPE_EMERGENCY, + COLOR_EMERGENCY_APPEAL, + APPEAL_TYPE_EAP, + COLOR_EAP, + APPEAL_TYPE_MULTIPLE, + COLOR_MULTIPLE_TYPES, + COLOR_BLACK, +]; +const basePointPaint: CirclePaint = { + 'circle-radius': 5, + 'circle-color': circleColor, + 'circle-opacity': 0.8, +}; + +export const basePointLayerOptions: Omit = { + type: 'circle', + paint: basePointPaint, +}; + +const baseOuterCirclePaint: CirclePaint = { + 'circle-color': circleColor, + 'circle-opacity': 0.4, +}; + +const outerCirclePaintForFinancialRequirements: CirclePaint = { + ...baseOuterCirclePaint, + 'circle-radius': [ + 'interpolate', + ['linear', 1], + ['get', 'financialRequirements'], + 1000, + 7, + 10000, + 9, + 100000, + 11, + 1000000, + 15, + ], +}; + +const outerCirclePaintForPeopleTargeted: CirclePaint = { + ...baseOuterCirclePaint, + 'circle-radius': [ + 'interpolate', + ['linear', 1], + ['get', 'peopleTargeted'], + 1000, + 7, + 10000, + 9, + 100000, + 11, + 1000000, + 15, + ], +}; + +export const outerCircleLayerOptionsForFinancialRequirements: Omit = { + type: 'circle', + paint: outerCirclePaintForFinancialRequirements, +}; + +export const outerCircleLayerOptionsForPeopleTargeted: Omit = { + type: 'circle', + paint: outerCirclePaintForPeopleTargeted, +}; + +export interface ScaleOption { + label: string; + value: 'financialRequirements' | 'peopleTargeted'; +} + +export function optionKeySelector(option: ScaleOption) { + return option.value; +} + +export function optionLabelSelector(option: ScaleOption) { + return option.label; +} + +export const appealTypeKeySelector = (option: AppealTypeOption) => option.key; +export const appealTypeLabelSelector = (option: AppealTypeOption) => option.value; + +export interface ClickedPoint { + featureProperties: AdminZeroFeatureProperties; + lngLat: mapboxgl.LngLatLike; +} + +export type Selector = { + key?: string; + id?: string; + label: string | null | undefined; +} + +export function keySelector(type: Selector) { + return type.key ?? ''; +} +export function labelSelector(type: Selector) { + return type.label ?? '?'; +} + +export function idSelector(type: Selector) { + return type.id ?? ''; +} diff --git a/app/views/CapacityAndResources/CapacityAndResourcesDetails/index.tsx b/app/views/CapacityAndResources/CapacityAndResourcesDetails/index.tsx new file mode 100644 index 0000000..a1059fd --- /dev/null +++ b/app/views/CapacityAndResources/CapacityAndResourcesDetails/index.tsx @@ -0,0 +1,63 @@ +import { useParams } from 'react-router'; +import { + Container, + ListView, +} from '@ifrc-go/ui'; +import { gql } from 'urql'; + +import Page from '#components/Page'; +import PowerBIEmbed from '#components/PowerBiEmbed'; +import { useCapacityAndResourceQuery } from '#generated/types/graphql'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const CAPACITY_AND_RESOURCES_DETAIL_QUERY = gql` + query CapacityAndResource( + $id: ID! + ) { + capacityAndResource(id: $id) { + title + id + description + dashboards { + url + id + title + description + } + } + } +`; + +export default function CapacityAndResourcesDetails() { + const { id } = useParams<{ id: string }>(); + + const [{ data, fetching: pending }] = useCapacityAndResourceQuery({ + variables: { id: id! }, + pause: !id, + }); + + const resourceData = data?.capacityAndResource; + + return ( + + + + {resourceData?.dashboards && resourceData?.dashboards.map((items) => ( + + ))} + + + + + ); +} diff --git a/app/views/CapacityAndResources/index.tsx b/app/views/CapacityAndResources/index.tsx new file mode 100644 index 0000000..db42f31 --- /dev/null +++ b/app/views/CapacityAndResources/index.tsx @@ -0,0 +1,153 @@ +import { SearchLineIcon } from '@ifrc-go/icons'; +import { + Container, + Pager, + Table, + TextInput, +} from '@ifrc-go/ui'; +import { + createElementColumn, + createStringColumn, +} from '@ifrc-go/ui/utils'; +import { gql } from 'urql'; + +import Link from '#components/Link'; +import Page from '#components/Page'; +import RegionSelectInput from '#components/RegionSelectInput'; +import { + type CapacityAndResourcesQuery, + useCapacityAndResourcesQuery, +} from '#generated/types/graphql'; +import useFilterState from '#hooks/useFilterState'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const CAPACITY_AND_RESOURCES_QUERY = gql` + query CapacityAndResources( + $limit: Int = 10 + $offset: Int = 0 + $search: String = "" + $regionId: ID + $isActive: Boolean + ) { + capacityAndResources( + pagination: { limit: $limit, offset: $offset } + filters: { search: $search, regionId: $regionId, isActive: $isActive } + ) { + totalCount + results { + title + id + } + } + } +`; +const capacityKeySelector = (item: CapacityAndResourcesList) => item.id; + +type CapacityAndResourcesList = NonNullable[number]; + +function ResourcesActions({ id }: {id: string}) { + return ( + + View Details + + ); +} + +function CapacityAndResourcesList() { + const { + limit, + page, + rawFilter, + filter, + setFilterField, + setPage, + offset, + } = useFilterState<{ + regionId?: string, + searchText?: string + }>({ + filter: {}, + pageSize: 6, + }); + const [{ data, fetching }] = useCapacityAndResourcesQuery({ + variables: { + // regionId: '', + isActive: true, + search: filter.searchText, + limit, + offset, + }, + }); + const capacityAndResourcesData = data?.capacityAndResources.results ?? []; + const columns = [ + createStringColumn( + 'sn', + 'S.N.', + (item) => String(capacityAndResourcesData.indexOf(item) + 1), + { columnWidth: 20 }, + ), + createStringColumn( + 'title', + 'Capacity and Resources', + (dept) => dept?.title, + ), + createElementColumn( + 'action', + 'Actions', + ResourcesActions, + (_key, item) => ({ + id: item.id, + }), + ), + ]; + return ( + {}} + /> + )} + heading="Capacity and Resources" + description="Monitor and allocate capacity and resources effectively to support humanitarian operations and response efforts." + > + } + /> + )} + footerActions={( + + )} + empty={capacityAndResourcesData.length === 0} + > + + + + ); +} + +export default CapacityAndResourcesList; diff --git a/app/views/DataAndReport/AIsummary/index.tsx b/app/views/DataAndReport/AIsummary/index.tsx new file mode 100644 index 0000000..9f1fa17 --- /dev/null +++ b/app/views/DataAndReport/AIsummary/index.tsx @@ -0,0 +1,62 @@ +import { StarLineIcon } from '@ifrc-go/icons'; +import { + Description, + Heading, + InlineView, + ListView, +} from '@ifrc-go/ui'; + +import styles from './styles.module.css'; + +function AIsummary() { + // TODO: fetch ai summary data from backend and display here + return ( + + } + spacing="sm" + contentAlignment="center" + > + AI Summary + + + A cholera outbreak was declared in Arsi Zone, + Ethiopia on 17 May 2025 and is spreading to nearby areas. + By mid-June, 201 cases (92% severe) and 2 deaths were reported. + The outbreak is rapidly increasing, with over 62,000 people in need of assistance. + + + A cholera outbreak was declared in Arsi Zone, + Ethiopia on 17 May 2025 and is spreading to nearby areas. + By mid-June, 201 cases (92% severe) and 2 deaths were reported. + The outbreak is rapidly increasing, with over 62,000 people in need of assistance. + + + A cholera outbreak was declared in Arsi Zone, + Ethiopia on 17 May 2025 and is spreading to nearby areas. + By mid-June, 201 cases (92% severe) and 2 deaths were reported. + The outbreak is rapidly increasing, with over 62,000 people in need of assistance. + + + A cholera outbreak was declared in Arsi Zone, + Ethiopia on 17 May 2025 and is spreading to nearby areas. + By mid-June, 201 cases (92% severe) and 2 deaths were reported. + The outbreak is rapidly increasing, with over 62,000 people in need of assistance. + + + A cholera outbreak was declared in Arsi Zone, + Ethiopia on 17 May 2025 and is spreading to nearby areas. + By mid-June, 201 cases (92% severe) and 2 deaths were reported. + The outbreak is rapidly increasing, with over 62,000 people in need of assistance. + + + + ); +} + +export default AIsummary; diff --git a/app/views/DataAndReport/AIsummary/styles.module.css b/app/views/DataAndReport/AIsummary/styles.module.css new file mode 100644 index 0000000..f5012ec --- /dev/null +++ b/app/views/DataAndReport/AIsummary/styles.module.css @@ -0,0 +1,6 @@ +.ai-summary { + background: var(--go-ui-color-primary-red) ; + padding-top: var(--go-ui-spacing-4xl) !important; + height: 100%; + color: var(--go-ui-color-white); +} \ No newline at end of file diff --git a/app/views/DataAndReport/ReportDetail/index.tsx b/app/views/DataAndReport/ReportDetail/index.tsx new file mode 100644 index 0000000..a55af93 --- /dev/null +++ b/app/views/DataAndReport/ReportDetail/index.tsx @@ -0,0 +1,141 @@ +import { useParams } from 'react-router'; +import { + Container, + Description, + Heading, + InlineView, + ListView, + PageContainer, +} from '@ifrc-go/ui'; +import { encodeDate } from '@togglecorp/fujs'; +import { gql } from 'urql'; + +import PdfViewer from '#components/PdfViewer'; +import PowerBIEmbed from '#components/PowerBiEmbed'; +import { useReportQuery } from '#generated/types/graphql'; +import AIsummary from '#views/DataAndReport/AIsummary'; + +import styles from './styles.module.css'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const REPORT_QUERY = gql` + query Report($id: ID!) { + report(id: $id) { + contentType + description + disasterType + file { + name + size + url + } + publishedAt + owner + iframeUrl + id + title + regionId + } + } +`; + +function ReportDetail() { + const { id } = useParams<{ id: string }>(); + + const [{ fetching, data }] = useReportQuery({ + variables: { id: id! }, + pause: !id, + }); + + const reportData = data?.report; + + // TODO: add condition or ai summary + const aiSummaryAvailable = !reportData?.iframeUrl; + const publishedDate = new Date(reportData?.publishedAt); + const encodedPublishedDate = encodeDate(publishedDate); + + return ( + + + + + + + + Published Date: + + + {encodedPublishedDate} + + + )} + after={( + + + Published By: + + + {reportData?.owner ?? 'Anonymous'} + + + )} + /> + + + {reportData?.title} + + + {reportData?.description} + + + + {reportData?.file?.url + ? + : ( + + ) } + + {aiSummaryAvailable && ( +
+
+ +
+
+ )} + +
+
+ ); +} + +export default ReportDetail; diff --git a/app/views/DataAndReport/ReportDetail/styles.module.css b/app/views/DataAndReport/ReportDetail/styles.module.css new file mode 100644 index 0000000..09fbca7 --- /dev/null +++ b/app/views/DataAndReport/ReportDetail/styles.module.css @@ -0,0 +1,17 @@ +.page-container { + padding-top: 0 !important; + + .content { + padding-top: var(--go-ui-spacing-md) + } +} + +.details { + position: relative; +} + +.sticky-details { + position: sticky; + top: 0; + height: 100vh; +} diff --git a/app/views/DataAndReport/index.tsx b/app/views/DataAndReport/index.tsx new file mode 100644 index 0000000..b1d56ac --- /dev/null +++ b/app/views/DataAndReport/index.tsx @@ -0,0 +1,189 @@ +import { SearchLineIcon } from '@ifrc-go/icons'; +import { + Container, + Description, + ListView, + Pager, + SelectInput, + TextInput, +} from '@ifrc-go/ui'; +import { gql } from 'urql'; + +import Link from '#components/Link'; +import Page from '#components/Page'; +import RegionSelectInput from '#components/RegionSelectInput'; +import ReportCard from '#components/ReportCard'; +import { + useReportsQuery, + useThematicAreasQuery, +} from '#generated/types/graphql'; +import useFilterState from '#hooks/useFilterState'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const ThematicAreas_QUERY = gql` + query ThematicAreas { + thematicAreas { + totalCount + results { + name + id + } + } + } +`; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const Reports_QUERY = gql` + query Reports( + $thematicAreaId: ID + $limit: Int = 10 + $offset: Int = 0 + ) { + reports( + filters: { thematicAreaId: $thematicAreaId } + pagination: { limit: $limit, offset: $offset } + ) { + totalCount + results { + id + title + description + contentType + visibility + publishedAt + createdAt + owner + iframeUrl + thematicAreaId + regionId + disasterType + file { + name + size + url + } + coverImage { + name + size + url + } + } + } + } +`; + +type ThematicArea = { + id: string; + name: string; +}; + +const keySelector = (item: ThematicArea) => item.id; +const labelSelector = (item: ThematicArea) => item.name; + +function DataAndReport() { + const [{ data }] = useThematicAreasQuery(); + + const { + limit, + page, + rawFilter, + setFilterField, + setPage, + offset, + } = useFilterState<{ + thematicAreaId?: string, + searchText?: string + }>({ + filter: {}, + pageSize: 6, + }); + + const [{ data: reportsData, fetching }] = useReportsQuery({ + variables: { + thematicAreaId: rawFilter.thematicAreaId, + // TODO: add search filter in backend and uncomment below line + // search: rawFilter.search, + limit, + offset, + }, + }); + const thematicAreaOptions = data?.thematicAreas?.results ?? []; + const reportDetails = reportsData?.reports?.results ?? []; + + return ( + {}} + /> + )} + heading="Dataset Overview" + description="Explore historical data, research findings and operational reports to support informed decision-making and planning." + > + + + + } + /> + + + Showing all + {' '} + {reportsData?.reports.totalCount} + {' '} + Data & Reports + + + )} + empty={reportDetails.length === 0} + > + {reportDetails.map((report) => ( + + + + ))} + + + + ); +} + +export default DataAndReport; diff --git a/app/views/Galleries/Photos/index.tsx b/app/views/Galleries/Photos/index.tsx new file mode 100644 index 0000000..7864b66 --- /dev/null +++ b/app/views/Galleries/Photos/index.tsx @@ -0,0 +1,146 @@ +import React from 'react'; +import { + DownloadTwoFillIcon, + EyeFillIcon, +} from '@ifrc-go/icons'; +import { + Container, + IconButton, + Image, + ListView, + Pager, +} from '@ifrc-go/ui'; +import { isDefined } from '@togglecorp/fujs'; +import { gql } from 'urql'; + +import { useGalleryQuery } from '#generated/types/graphql'; +import useFilterState from '#hooks/useFilterState'; + +import styles from './styles.module.css'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const IMAGES_QUERY = gql` + query Gallery($offset: Int, $limit: Int, $albumId: ID) { + galleryImages( + pagination: { limit: $limit, offset: $offset } + filters: { albumId: $albumId } + ) { + totalCount + results { + albumId + caption + id + image { + name + size + url + } + } + } + } +`; +// For Local Development +const toSafeSrc = (src: string) => (src.startsWith('http') + ? src.replace(/^http:\/\/web:8000/, 'http://localhost:8000') + : src); + +interface ImageComponentProps { + src: string; + name: string; + onView: (src: string) => void; +} + +function ImageComponent(props: ImageComponentProps) { + const { + src, name, + onView, + } = props; + const safeSrc = toSafeSrc(src); + + return ( +
+ + + + + + + + + + + +
+ ); +} + +function Photos(props: {albumId: string, handleView: (src:string) => void}) { + const { albumId, handleView } = props; + const { + limit, + page, + setPage, + offset, + } = useFilterState({ + filter: {}, + pageSize: 9, + }); + + const [{ fetching: imageLoading, data: imageData }] = useGalleryQuery({ + variables: { + albumId, + limit, + offset, + }, + pause: !albumId, + }); + + return ( + + )} + empty={isDefined(imageData) && imageData.galleryImages.results.length === 0} + emptyMessage="No Image Available" + > + + {imageData?.galleryImages.results.map((item) => ( + + ))} + + + ); +} + +export default Photos; diff --git a/app/views/Galleries/Photos/styles.module.css b/app/views/Galleries/Photos/styles.module.css new file mode 100644 index 0000000..8d411cf --- /dev/null +++ b/app/views/Galleries/Photos/styles.module.css @@ -0,0 +1,23 @@ +.img-container { + position: relative; + transition: all 0.3s ease-in; + + .action-button { + position: absolute; + top: 4px; + right: 4px; + opacity: 0; + z-index: 1; + } + + &:hover { + transition: all 0.2s ease-out; + opacity: 0.85; + cursor: pointer; + + .action-button { + opacity: 1; + } + } + +} \ No newline at end of file diff --git a/app/views/Galleries/index.tsx b/app/views/Galleries/index.tsx new file mode 100644 index 0000000..1abc5b9 --- /dev/null +++ b/app/views/Galleries/index.tsx @@ -0,0 +1,215 @@ +import { + useEffect, + useState, +} from 'react'; +import { SearchLineIcon } from '@ifrc-go/icons'; +import { + Button, + Container, + Description, + Heading, + Image, + InlineView, + ListView, + Modal, + NavigationTabList, + TabLayout, + TextInput, +} from '@ifrc-go/ui'; +import { isNotDefined } from '@togglecorp/fujs'; +import { gql } from 'urql'; + +import Page from '#components/Page'; +import { + type AlbumsQuery, + useAlbumsQuery, +} from '#generated/types/graphql'; +import useFilterState from '#hooks/useFilterState'; +import Photos from '#views/Galleries/Photos'; + +import styles from './styles.module.css'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const ALBUM_QUERY = gql` + query Albums($offset: Int, $limit: Int, $search: String) { + galleryAlbums( + pagination: { limit: $limit, offset: $offset } + filters: { search: $search } + ) { + results { + title + updatedAt + id + description + } + totalCount + } + } +`; +interface ImageViewerProps { + src: string; + onClose: () => void; +} + +function ImageViewer(props: ImageViewerProps) { + const { src, onClose } = props; + return ( + + + + ); +} + +type AlbumList = NonNullable[number]; + +function Galleries() { + const [activeId, setActiveId] = useState(''); + const [selectedImage, setSelectedImage] = useState(''); + const [viewerOpen, setViewerOpen] = useState(false); + const [albumData, setAlbumData] = useState([]); + const albumId = activeId || albumData[0]?.id || ''; + const { + filter, + limit, + page, + rawFilter, + setFilterField, + setPage, + offset, + } = useFilterState<{ + searchText?: string + }>({ + filter: {}, + pageSize: 5, + }); + + const [{ data, fetching }] = useAlbumsQuery(({ + variables: { + search: filter.searchText, + limit, + offset, + }, + })); + const handleView = (src: string) => { + setSelectedImage(src); + setViewerOpen(true); + }; + + useEffect(() => { + if (filter.searchText) { + // eslint-disable-next-line react-hooks/set-state-in-effect + setAlbumData(data?.galleryAlbums.results ?? []); + } + if (!data?.galleryAlbums?.results?.length) return; + setAlbumData((prev) => { + const existingIds = new Set(prev.map((a) => a.id)); + const incoming = data.galleryAlbums.results.filter((a) => !existingIds.has(a.id)); + return incoming.length ? [...prev, ...incoming] : prev; + }); + }, [data, filter.searchText]); + + return ( + + + {data?.galleryAlbums.totalCount} + {' '} + Events + + + )} + > + + {viewerOpen && ( + setViewerOpen(false)} + /> + )} + + Events + + + + } + /> + + + + {albumData.map((item) => ( + setActiveId(item.id)} + > + {item.title} + + ))} + + + {(data?.galleryAlbums?.totalCount ?? 0) + > albumData.length && ( + { + setPage(page + 1); + }} + styleVariant="action" + > + show more + + )} + /> + )} + + + + + + ); +} + +export default Galleries; diff --git a/app/views/Galleries/styles.module.css b/app/views/Galleries/styles.module.css new file mode 100644 index 0000000..b0868eb --- /dev/null +++ b/app/views/Galleries/styles.module.css @@ -0,0 +1,37 @@ +.img-container { + position: relative; + + .action-button { + position: absolute; + top: 4px; + right: 4px; + opacity: 0; + z-index: 999; + } + + transition: all 0.3s ease-in; + + &:hover { + .action-button { + opacity: 1; + } + + transition: all 0.2s ease-out; + opacity: 0.85; + cursor: pointer; + } +} + +.image { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; +} + +.img { + max-width: 100%; + max-height: 100%; + object-fit: contain; +} \ No newline at end of file diff --git a/app/views/GuestLayout/index.tsx b/app/views/GuestLayout/index.tsx index 83cdb8f..370acf1 100644 --- a/app/views/GuestLayout/index.tsx +++ b/app/views/GuestLayout/index.tsx @@ -6,17 +6,13 @@ import { import UserContext from '#contexts/UserContext'; -import styles from './styles.module.css'; - function GuestLayout() { const { authenticated } = use(UserContext); if (authenticated) { return ; } return ( -
- -
+ ); } diff --git a/app/views/GuestLayout/styles.module.css b/app/views/GuestLayout/styles.module.css deleted file mode 100644 index 650d026..0000000 --- a/app/views/GuestLayout/styles.module.css +++ /dev/null @@ -1,4 +0,0 @@ -.guest-layout { - display: flex; - flex: 1; -} diff --git a/app/views/Home/ActiveOperation/index.tsx b/app/views/Home/ActiveOperation/index.tsx new file mode 100644 index 0000000..1a681f8 --- /dev/null +++ b/app/views/Home/ActiveOperation/index.tsx @@ -0,0 +1,536 @@ +import { + use, + useState, +} from 'react'; +import { + Button, + Container, + DateInput, + LegendItem, + ListView, + Pager, + RadioInput, + SelectInput, + Table, + TextOutput, +} from '@ifrc-go/ui'; +import { SortContext } from '@ifrc-go/ui/contexts'; +import { + createDateColumn, + createProgressColumn, + createStringColumn, + getPercentage, + hasSomeDefinedValue, + resolveToComponent, + sumSafe, +} from '@ifrc-go/ui/utils'; +import { + isDefined, + isNotDefined, + listToGroupList, + mapToMap, + unique, +} from '@togglecorp/fujs'; +import { + MapBounds, + MapLayer, + MapSource, +} from '@togglecorp/re-map'; +import type { LngLatBoundsLike } from 'mapbox-gl'; + +import DisasterTypeSelectInput from '#components/DisasterTypeSelectInput'; +import GlobalMap, { type AdminZeroFeatureProperties } from '#components/GlobalMap'; +import GoMapContainer from '#components/GoMapContainer'; +import Link from '#components/Link'; +import MapPopup from '#components/MapPopup'; +import { goUrl } from '#config'; +import GoContext from '#contexts/GoContext'; +import useFilterState from '#hooks/useFilterState'; +import useInputState from '#hooks/useInputState'; +import { + DEFAULT_MAP_PADDING, + DURATION_MAP_ZOOM, +} from '#utils/constants'; +import { getGeoJsonBounds } from '#utils/geo'; +import { + type GoApiResponse, + type GoApiUrlQuery, + useRequest, +} from '#utils/restRequest'; +import { + createAppealCodeColumn, + createBudgetColumn, + createDisasterTypeColumn, + createEventColumn, +} from '#utils/tableHelpers'; +import { + APPEAL_TYPE_DREF, + APPEAL_TYPE_EAP, + APPEAL_TYPE_EMERGENCY, + APPEAL_TYPE_MULTIPLE, + appealTypeKeySelector, + appealTypeLabelSelector, + basePointLayerOptions, + type ClickedPoint, + COLOR_DREF, + COLOR_EAP, + COLOR_EMERGENCY_APPEAL, + COLOR_MULTIPLE_TYPES, + optionKeySelector, + optionLabelSelector, + outerCircleLayerOptionsForFinancialRequirements, + outerCircleLayerOptionsForPeopleTargeted, + type ScaleOption, +} from '#utils/utils'; + +type GlobalEnumsResponse = GoApiResponse<'/api/v2/global-enums/'>; +type AppealTypeOption = NonNullable[number]; + +type AppealQueryParams = GoApiUrlQuery<'/api/v2/appeal/'>; +type AppealResponse = GoApiResponse<'/api/v2/appeal/'>; +type AppealListItem = NonNullable[number]; + +const appealKeySelector = (option: AppealListItem) => option.id; + +const sourceOptions: mapboxgl.GeoJSONSourceRaw = { + type: 'geojson', +}; + +const now = new Date().toISOString(); + +function ActiveOperation() { + const { + countryResponse: countryData, + countryId, + globalEnums, + } = use(GoContext); + const [scaleBy, setScaleBy] = useInputState('peopleTargeted'); + const [presentationMode, setPresentationMode] = useState(false); + const { + filter, + filtered, + limit, + page, + rawFilter, + setFilter, + setFilterField, + setPage, + sortState, + offset, + } = useFilterState<{ + appeal?: AppealTypeOption['key'], + district?: number[], + displacement?: number, + startDateAfter?: string, + startDateBefore?: string, + }>({ + filter: {}, + pageSize: 5, + }); + + const isFiltered = hasSomeDefinedValue(rawFilter); + + const queryParams: AppealQueryParams = { + atype: filter.appeal, + dtype: filter.displacement, + district: hasSomeDefinedValue(filter.district) ? filter.district : undefined, + end_date__gt: now, + start_date__gte: filter.startDateAfter, + start_date__lte: filter.startDateBefore, + limit, + offset, + region: undefined, + country: [countryId], + }; + const [ + clickedPoint, + setClickedPoint, + ] = useState(); + + const { + pending: appealsPending, + response: appealsResponse, + error: appealsResponseError, + } = useRequest({ + url: '/api/v2/appeal/', + preserveResponse: true, + query: queryParams, + }); + + const countryGroupedAppeal = listToGroupList( + appealsResponse?.results ?? [], + (appeal) => appeal.country.iso3 ?? '', + ); + + const countryCentroidGeoJson = (): GeoJSON.FeatureCollection => { + const countryToOperationTypeMap = mapToMap( + countryGroupedAppeal, + (key) => key, + (appealList) => { + const uniqueAppealList = unique( + appealList.map((appeal) => appeal.atype), + ); + + const peopleTargeted = sumSafe( + appealList.map((appeal) => appeal.num_beneficiaries), + ); + const financialRequirements = sumSafe( + appealList.map((appeal) => appeal.amount_requested), + ); + + if (uniqueAppealList.length > 1) { + return { + appealType: APPEAL_TYPE_MULTIPLE, + peopleTargeted, + financialRequirements, + }; + } + + return { + appealType: uniqueAppealList[0], + peopleTargeted, + financialRequirements, + }; + }, + ); + + const iso3 = countryData?.iso3; + const operation = iso3 ? countryToOperationTypeMap[iso3] : undefined; + + return { + type: 'FeatureCollection' as const, + features: (countryData ? [countryData] : []) + ?.map((country) => { + if ( + (!country.independent) + || isNotDefined(country.centroid) + || isNotDefined(country.iso3) + ) { + return undefined; + } + + if (isNotDefined(operation)) { + return undefined; + } + + return { + type: 'Feature' as const, + geometry: country.centroid as { + type: 'Point', + coordinates: [number, number], + }, + properties: { + id: country.iso3, + appealType: operation.appealType, + peopleTargeted: operation.peopleTargeted, + financialRequirements: operation.financialRequirements, + }, + }; + }).filter(isDefined) ?? [], + }; + }; + + const scaleOptions: ScaleOption[] = ([ + { value: 'peopleTargeted', label: '# of people targeted' }, + { value: 'financialRequirements', label: 'IFRC financial requirements' }, + ]); + + const legendOptions = ([ + { + value: APPEAL_TYPE_EMERGENCY, + label: 'Emergency Appeal', + color: COLOR_EMERGENCY_APPEAL, + }, + { + value: APPEAL_TYPE_DREF, + label: 'DREF', + color: COLOR_DREF, + }, + { + value: APPEAL_TYPE_EAP, + label: 'Early Action Protocol Activation', + color: COLOR_EAP, + }, + { + value: APPEAL_TYPE_MULTIPLE, + label: 'Multiple Types', + color: COLOR_MULTIPLE_TYPES, + }, + ]); + + const countryBounds :LngLatBoundsLike | undefined = (countryData && countryData.bbox) + ? getGeoJsonBounds(countryData.bbox) + : undefined; + const heading = resolveToComponent( + 'Active Operations Map ({numAppeals})', + { numAppeals: appealsResponse?.count ?? 0 }, + ); + + const popupDetails = clickedPoint + ? countryGroupedAppeal[clickedPoint.featureProperties.iso3] + : undefined; + + const handlePointClose = () => { + setClickedPoint(undefined); + }; + + const handleCountryClick = ( + featureProperties: AdminZeroFeatureProperties, + lngLat: mapboxgl.LngLatLike, + ) => { + setClickedPoint({ + featureProperties, + lngLat, + }); + + return true; + }; + + const columns = [ + createDateColumn( + 'start_date', + 'Start Date', + (item) => item.start_date, + { sortable: true }, + ), + createStringColumn( + 'atype', + 'Appeal Type', + (item) => item.atype_display, + { sortable: true }, + ), + createAppealCodeColumn( + 'code', + 'Code', + (item) => item.code, + ), + createEventColumn( + 'operation', + 'operation', + (item) => item.name, + (item) => ({ + href: `${goUrl}/emergencies/${item.event}/details`, + external: true, + }), + ), + createDisasterTypeColumn( + 'dtype', + 'Disater Type', + (item) => item.dtype?.name, + { sortable: true }, + ), + createBudgetColumn( + 'amount_requested', + 'Amount Requested', + (item) => item.amount_requested, + { sortable: true }, + ), + createProgressColumn( + 'amount_funded', + 'Amount funded', + // FIXME: use progress function + (item) => ( + getPercentage( + item.amount_funded, + item.amount_requested, + ) + ), + { sortable: true }, + ), + ].filter(isDefined); + + const handleClearFiltersButtonClick = (() => { + setFilter({}); + }); + return ( + + View all Emergencies + + )} + filters={( + <> + + + + + + + )} + footerActions={( + + )} + > + + + + + {legendOptions.map((legendItem) => ( + + ))} + + + )} + /> + + + + + {clickedPoint?.lngLat && ( + + + {popupDetails?.map( + (appeal) => ( + + + + + + + + ), + )} + + + )} + {isDefined(countryBounds) && ( + + )} + + +
+ + + ); +} + +export default ActiveOperation; diff --git a/app/views/Home/index.tsx b/app/views/Home/index.tsx index 72528cc..f832b55 100644 --- a/app/views/Home/index.tsx +++ b/app/views/Home/index.tsx @@ -1,8 +1,152 @@ +import { + AlarmWarningLineIcon, + AlertLineIcon, + DashboardFillIcon, + HeartAddLineIcon, + ShieldUserLineIcon, +} from '@ifrc-go/icons'; +import { + Container, + ListView, +} from '@ifrc-go/ui'; -const Home = () => { +import InfoCard from '#components/InfoCard'; +import KeyCard from '#components/KeyCard'; +import Page from '#components/Page'; +import ActiveOperation from '#views/Home/ActiveOperation'; + +function Home() { + // TODO: Fetch real data for key figures and operations + const keyFigures = ( + + } + value={12} + valueType="number" + size="lg" + label="Emergencies" + info="in last 30 days" + + /> + } + value={250} + valueType="number" + size="lg" + label="People reached" + info="in last 30 days" + + /> + } + value={18} + valueType="number" + size="lg" + valueOptions={{ compact: true }} + label="population Affected" + info="in last 30 days" + + /> + } + value={18} + valueType="number" + label="People in Need" + size="lg" + info="in last 30 days" + /> + + ); return ( -
Home
- ) + + {keyFigures} + + )} + > + + + } + title="Operational Dashboards" + description="Real-time emergency alerts and early warning system monitoring across regions" + /> + + + + + + + + + + + ); } -export default Home \ No newline at end of file +export default Home; diff --git a/app/views/Login/index.tsx b/app/views/Login/index.tsx new file mode 100644 index 0000000..664dc22 --- /dev/null +++ b/app/views/Login/index.tsx @@ -0,0 +1,159 @@ +import { + Button, + Container, + Description, + Heading, + Image, + InlineLayout, + ListView, + PageContainer, + PasswordInput, + TextInput, +} from '@ifrc-go/ui'; +import { + createSubmitHandler, + getErrorObject, + type ObjectSchema, + requiredStringCondition, + useForm, +} from '@togglecorp/toggle-form'; + +import BackGroundImage from '#resources/image/loginbackground.jpg'; +import Logo from '#resources/image/logo.png'; + +import styles from './styles.module.css'; + +interface FormFields { + username?: string; + password?: string; +} +type FormSchema = ObjectSchema; +type FormSchemaFields = ReturnType; + +const defaultFormValue: FormFields = { +}; + +const formSchema: FormSchema = { + fields: (): FormSchemaFields => ({ + username: { + required: true, + requiredValidation: requiredStringCondition, + }, + password: { + required: true, + requiredValidation: requiredStringCondition, + }, + }), +}; + +function Login() { + const { + value: formValue, + error: formError, + setFieldValue, + setError, + validate, + } = useForm(formSchema, { value: defaultFormValue }); + + const fieldError = getErrorObject(formError); + + // TODO: Implement actual login logic + const login = () => {}; + + const handleFormSubmit = () => createSubmitHandler( + validate, + setError, + login, + ); + + return ( + + + +
+ + + + + + + + ERCS EOC + + + Login with + your ERCS email and password. + + + + + + + + + + + + + + + +
+
+ ); +} + +export default Login; diff --git a/app/views/Login/styles.module.css b/app/views/Login/styles.module.css new file mode 100644 index 0000000..0e2a698 --- /dev/null +++ b/app/views/Login/styles.module.css @@ -0,0 +1,23 @@ +.image { + filter: grayscale(100%); + +} + +.separator { + border-bottom:var(--go-ui-width-separator-sm) solid var(--go-ui-color-gray-20); +} + +.container { + justify-content: center; + height: 100%; + + .login { + height: 100%; + + } +} + +.logo { + width: 70px; + height: 70px; +} \ No newline at end of file diff --git a/app/views/OurWork/EmergencyResponse/index.tsx b/app/views/OurWork/EmergencyResponse/index.tsx new file mode 100644 index 0000000..1ef5940 --- /dev/null +++ b/app/views/OurWork/EmergencyResponse/index.tsx @@ -0,0 +1,78 @@ +import { AlertLineIcon } from '@ifrc-go/icons'; +import { + Container, + ListView, +} from '@ifrc-go/ui'; +import { gql } from 'urql'; + +import InfoCard from '#components/InfoCard'; +import PowerBIEmbed from '#components/PowerBiEmbed'; +import { useExternalDashboardsQuery } from '#generated/types/graphql'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const ExternalDashboards_QUERY = gql` + query ExternalDashboards( + $page: String = "" + $isActive: Boolean = true + ) { + externalDashboards( + filters: { page: $page, isActive: $isActive } + ) { + results { + title + updatedAt + order + id + isActive + createdAt + description + page + regionId + showOnHome + url + } + pageInfo { + limit + offset + } + totalCount + } + } +`; + +function EmergencyResponse() { + // Todo: Region filter + const [{ data: emergencyResponse, fetching }] = useExternalDashboardsQuery({ + variables: { + // NOTE: Page variable value based on page enum where 70 is emergency response + page: '70', + isActive: true, + }, + + }); + return ( + + + } + title="Emergency Response Overview Dashboard" + description="Real-time emergency alerts and early warning system monitoring across regions" + /> + {emergencyResponse?.externalDashboards.results.map((report) => ( + + ))} + + + ); +} + +export default EmergencyResponse; diff --git a/app/views/OurWork/ProjectMapping/index.tsx b/app/views/OurWork/ProjectMapping/index.tsx new file mode 100644 index 0000000..03690d3 --- /dev/null +++ b/app/views/OurWork/ProjectMapping/index.tsx @@ -0,0 +1,78 @@ +import { AlertLineIcon } from '@ifrc-go/icons'; +import { + Container, + ListView, +} from '@ifrc-go/ui'; +import { gql } from 'urql'; + +import InfoCard from '#components/InfoCard'; +import PowerBIEmbed from '#components/PowerBiEmbed'; +import { useExternalDashboardsQuery } from '#generated/types/graphql'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const ExternalDashboards_QUERY = gql` + query ExternalDashboards( + $page: String = "" + $isActive: Boolean = true + ) { + externalDashboards( + filters: { page: $page, isActive: $isActive } + ) { + results { + title + updatedAt + order + id + isActive + createdAt + description + page + regionId + showOnHome + url + } + pageInfo { + limit + offset + } + totalCount + } + } +`; + +function ProjectMapping() { + // Todo: Region filter + const [{ data: projectMappingData, fetching }] = useExternalDashboardsQuery({ + variables: { + // NOTE: Page variable value based on page enum where 30 is project mapping + page: '30', + isActive: true, + }, + + }); + return ( + + + } + title="Emergency Response Overview Dashboard" + description="Real-time emergency alerts and early warning system monitoring across regions" + /> + {projectMappingData?.externalDashboards.results.map((report) => ( + + ))} + + + ); +} + +export default ProjectMapping; diff --git a/app/views/OurWork/index.tsx b/app/views/OurWork/index.tsx new file mode 100644 index 0000000..e49d2be --- /dev/null +++ b/app/views/OurWork/index.tsx @@ -0,0 +1,127 @@ +import { Outlet } from 'react-router'; +import { + AlertLineIcon, + HeartAddLineIcon, + ShieldUserLineIcon, +} from '@ifrc-go/icons'; +import { + Container, + ListView, + NavigationTabList, +} from '@ifrc-go/ui'; +import { gql } from 'urql'; + +import KeyCard from '#components/KeyCard'; +import NavigationTab from '#components/NavigationTab'; +import Page from '#components/Page'; +import RegionSelectInput from '#components/RegionSelectInput'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const ExternalDashboards_QUERY = gql` + query ExternalDashboards( + $page: String = "" + $isActive: Boolean = true + ) { + externalDashboards( + filters: { page: $page, isActive: $isActive } + ) { + results { + title + updatedAt + order + id + isActive + createdAt + description + page + regionId + showOnHome + url + } + pageInfo { + limit + offset + } + totalCount + } + } +`; + +// TODO: Fetch real data for key figures and operations + +const keyFigures = ( + + } + value={250} + valueType="number" + size="lg" + label="People Reached" + info="in last 30 days" + /> + } + value={18} + valueType="number" + size="lg" + valueOptions={{ compact: true }} + label="Population Affected" + info="in last 30 days" + /> + } + value={18} + valueType="number" + size="lg" + label="People in Need" + info="in last 30 days" + /> + +); + +function OurWork() { + return ( + + {keyFigures} + + )} + actions={( + // TODO: add region filter + {}} + /> + )} + > + + + + Emergency Response + + + Project Mapping + + + + + + + ); +} + +export default OurWork; diff --git a/app/views/Preparedness/AllEmergencies/index.tsx b/app/views/Preparedness/AllEmergencies/index.tsx new file mode 100644 index 0000000..0a06337 --- /dev/null +++ b/app/views/Preparedness/AllEmergencies/index.tsx @@ -0,0 +1,274 @@ +import { useMemo } from 'react'; +import { + Container, + DateInput, + Description, + NumberOutput, + Pager, + Table, +} from '@ifrc-go/ui'; +import { SortContext } from '@ifrc-go/ui/contexts'; +import { + createDateColumn, + createNumberColumn, + createStringColumn, + resolveToComponent, + sumSafe, +} from '@ifrc-go/ui/utils'; +import { + isDefined, + max, +} from '@togglecorp/fujs'; +import { saveAs } from 'file-saver'; +import Papa from 'papaparse'; + +import DisasterTypeSelectInput from '#components/DisasterTypeSelectInput'; +import ExportButton from '#components/ExportButton'; +import { goUrl } from '#config'; +import useAlert from '#hooks/useAlert'; +import useFilterState from '#hooks/useFilterState'; +import useRecursiveCSVRequest from '#hooks/useRecursiveCsvRequest'; +import useUrlSearchState from '#hooks/useUrlSearchState'; +import { + type GoApiResponse, + type GoApiUrlQuery, + useRequest, +} from '#utils/restRequest'; +import { createLinkColumn } from '#utils/tableHelpers'; + +import styles from './styles.module.css'; + +type EventResponse = GoApiResponse<'/api/v2/event/'>; +type EventQueryParams = GoApiUrlQuery<'/api/v2/event/'>; +type EventListItem = NonNullable[number]; + +function getMostRecentAffectedValue(fieldReport: EventListItem['field_reports']) { + const latestReport = max(fieldReport, (item) => new Date(item.updated_at).getTime()); + return latestReport?.num_affected; +} + +const eventKeySelector = (item: EventListItem) => item.id; + +function AllEmergency() { + const { + sortState, + ordering, + page, + setPage, + limit, + offset, + rawFilter, + filter, + setFilterField, + filtered, + } = useFilterState<{ + startDateAfter?: string, + startDateBefore?: string, + }>({ + filter: {}, + pageSize: 10, + }); + const alert = useAlert(); + + const columns = [ + createDateColumn( + 'disaster_start_date', + 'Start Date', + (item) => item.disaster_start_date, + { + sortable: true, + columnClassName: styles.createdAt, + }, + ), + createLinkColumn( + 'event_name', + 'Name', + (item) => item.name, + (item) => ({ + href: `${goUrl}/emergencies/${item.id}/details`, + external: true, + }), + { sortable: true }, + ), + createStringColumn( + 'dtype', + 'Disaster Type', + (item) => item.dtype?.name, + ), + createStringColumn( + 'glide', + 'Glide', + (item) => item.glide, + { sortable: true }, + ), + createNumberColumn( + 'amount_requested', + 'Requested Amount', + (item) => sumSafe( + item.appeals.map((appeal) => appeal.amount_requested), + ), + { + suffix: ' CHF', + }, + ), + createNumberColumn( + 'num_affected', + '# Affected', + (item) => item.num_affected ?? getMostRecentAffectedValue(item.field_reports), + { sortable: true }, + ), + ]; + + const [filterDisasterType, setFilterDisasterType] = useUrlSearchState( + 'dtype', + (searchValue) => { + const potentialValue = isDefined(searchValue) ? Number(searchValue) : undefined; + return potentialValue; + }, + (dtype) => (isDefined(dtype) ? String(dtype) : undefined), + ); + + const query = useMemo( + () => ({ + limit, + offset, + ordering, + dtype: filterDisasterType, + // FIXME: The server should actually accept array of number instead + // of just number + countries__in: 65, + disaster_start_date__gte: filter.startDateAfter, + disaster_start_date__lte: filter.startDateBefore, + }), + [ + limit, + offset, + ordering, + filterDisasterType, + filter, + ], + ); + + const { + pending: eventPending, + response: eventResponse, + } = useRequest({ + url: '/api/v2/event/', + preserveResponse: true, + query, + }); + + const heading = useMemo( + () => resolveToComponent( + 'All Emergencies ({numEmergencies})', + { + numEmergencies: ( + + ), + }, + ), + [eventResponse], + ); + + const [ + pendingExport, + progress, + triggerExportStart, + ] = useRecursiveCSVRequest({ + onFailure: () => { + alert.show( + 'Failed to generate export.', + { variant: 'danger' }, + ); + }, + onSuccess: (data) => { + const unparseData = Papa.unparse(data); + const blob = new Blob( + [unparseData], + { type: 'text/csv' }, + ); + saveAs(blob, 'all-emergencies.csv'); + }, + }); + + const handleExportClick = (() => { + if (!eventResponse?.count) { + return; + } + triggerExportStart( + '/api/v2/event/', + eventResponse?.count, + query, + ); + }); + + const isFiltered = isDefined(filterDisasterType) || filtered; + + return ( + + Explore historical data, research findings and + operational reports to support informed decision-making and planning. + + )} + headerActions={( + + )} + filters={( + <> + + + + + )} + footerActions={( + + )} + > + +
+ + + ); +} + +export default AllEmergency; diff --git a/app/views/Preparedness/AllEmergencies/styles.module.css b/app/views/Preparedness/AllEmergencies/styles.module.css new file mode 100644 index 0000000..c45fb05 --- /dev/null +++ b/app/views/Preparedness/AllEmergencies/styles.module.css @@ -0,0 +1,8 @@ +.all-emergencies { + .table { + .created-at { + width: 0%; + min-width: 9rem; + } + } +} diff --git a/app/views/Preparedness/DisasterResponse/index.tsx b/app/views/Preparedness/DisasterResponse/index.tsx new file mode 100644 index 0000000..bf47087 --- /dev/null +++ b/app/views/Preparedness/DisasterResponse/index.tsx @@ -0,0 +1,46 @@ +import { AlertLineIcon } from '@ifrc-go/icons'; +import { + Container, + ListView, +} from '@ifrc-go/ui'; + +import InfoCard from '#components/InfoCard'; +import PowerBIEmbed from '#components/PowerBiEmbed'; +import { useExternalDashboardsQuery } from '#generated/types/graphql'; + +function DisasterResponse() { + // Todo: Region filter + const [{ data: disasterResponse, fetching }] = useExternalDashboardsQuery({ + + variables: { + // NOTE: Page variable value based on page enum where 60 is disaster response + page: '60', + isActive: true, + }, + + }); + return ( + + + } + title="Alerts Dashboard" + description="Real-time emergency alerts and early warning system monitoring across regions" + /> + {disasterResponse?.externalDashboards.results.map((report) => ( + + ))} + + + ); +} +export default DisasterResponse; diff --git a/app/views/Preparedness/EmergencyAlert/index.tsx b/app/views/Preparedness/EmergencyAlert/index.tsx new file mode 100644 index 0000000..2932729 --- /dev/null +++ b/app/views/Preparedness/EmergencyAlert/index.tsx @@ -0,0 +1,45 @@ +import { AlertLineIcon } from '@ifrc-go/icons'; +import { + Container, + ListView, +} from '@ifrc-go/ui'; + +import InfoCard from '#components/InfoCard'; +import PowerBIEmbed from '#components/PowerBiEmbed'; +import { useExternalDashboardsQuery } from '#generated/types/graphql'; + +function EmergencyAlert() { + // Todo: Region filter + const [{ data: emergencyAlert, fetching }] = useExternalDashboardsQuery({ + variables: { + // NOTE: Page variable value based on page enum where 50 is emergency alert + page: '50', + isActive: true, + }, + + }); + return ( + + + } + title="Alerts Dashboard" + description="Real-time emergency alerts and early warning system monitoring across regions" + /> + {emergencyAlert?.externalDashboards.results.map((report) => ( + + ))} + + + ); +} +export default EmergencyAlert; diff --git a/app/views/Preparedness/RiskAnalysis/CountryRiskSourcesOutput/index.tsx b/app/views/Preparedness/RiskAnalysis/CountryRiskSourcesOutput/index.tsx new file mode 100644 index 0000000..faad35b --- /dev/null +++ b/app/views/Preparedness/RiskAnalysis/CountryRiskSourcesOutput/index.tsx @@ -0,0 +1,64 @@ +import { Fragment } from 'react'; +import { TextOutput } from '@ifrc-go/ui'; +import { resolveToComponent } from '@ifrc-go/ui/utils'; + +import Link from '#components/Link'; + +function CountryRiskSourcesOutput() { + const riskByMonthSources = [ + { + key: 'inform', + link: 'https://drmkc.jrc.ec.europa.eu/inform-index/INFORM-Risk', + label: 'INFORM', + description: "{link} for each country's level of risk", + }, + { + key: 'undrr', + link: 'https://www.undrr.org/', + label: 'UNDRR', + description: '{link} for the population exposure', + }, + { + key: 'idmc', + link: 'https://www.internal-displacement.org/', + label: 'IDMC', + description: '{link} for the expected displacements', + }, + { + key: 'ipc', + link: 'https://www.ipcinfo.org/', + label: 'IPC', + description: '{link} for food insecurity', + }, + ]; + + return ( + ( + + {resolveToComponent( + source.description, + { + link: ( + + {source.label} + + ), + }, + )} + {i < (riskByMonthSources.length - 1) &&
} +
+ )) + } + /> + ); +} + +export default CountryRiskSourcesOutput; diff --git a/app/views/Preparedness/RiskAnalysis/MultiMonthSelectInput/index.tsx b/app/views/Preparedness/RiskAnalysis/MultiMonthSelectInput/index.tsx new file mode 100644 index 0000000..78d4d2a --- /dev/null +++ b/app/views/Preparedness/RiskAnalysis/MultiMonthSelectInput/index.tsx @@ -0,0 +1,170 @@ +import { + useCallback, + useEffect, + useRef, +} from 'react'; +import { RawButton } from '@ifrc-go/ui'; +import { + _cs, + isNotDefined, + listToMap, +} from '@togglecorp/fujs'; +import type { SetValueArg } from '@togglecorp/toggle-form'; + +import { + monthKeyList, + multiMonthSelectDefaultValue, +} from '#utils/constants'; + +import styles from './styles.module.css'; + +interface Props { + className?: string; + value: Record | undefined; + name: NAME; + onChange: ( + newValue: SetValueArg | undefined>, + name: NAME, + ) => void; +} + +function MultiMonthSelectInput(props: Props) { + const { + className, + name, + value, + onChange, + } = props; + + const shiftPressedRef = useRef(false); + + useEffect( + () => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Shift') { + shiftPressedRef.current = true; + } + }; + + const handleKeyUp = () => { + shiftPressedRef.current = false; + }; + + document.addEventListener('keyup', handleKeyUp); + document.addEventListener('keydown', handleKeyDown); + + return () => { + document.removeEventListener('keyup', handleKeyUp); + document.removeEventListener('keydown', handleKeyDown); + }; + }, + [], + ); + + const handleClick = useCallback( + (month: number) => { + if (isNotDefined(onChange)) { + return; + } + + onChange( + (prevValue) => { + const prevValueList = Object.values(prevValue ?? {}); + const numTruthyValues = prevValueList.filter(Boolean).length; + + if (month === 12 + || !shiftPressedRef.current + || numTruthyValues === 0 + || prevValue?.[12] + // Clicked on previously selected month + || (numTruthyValues === 1 && prevValue?.[month]) + ) { + // Selecting only single value + return { + ...multiMonthSelectDefaultValue, + [month]: true, + }; + } + + const truthyValueStartIndex = prevValueList.findIndex(Boolean); + const newValueList = [...prevValueList]; + const lengthDiff = Math.abs(month - truthyValueStartIndex); + // Fill selection start to end with true + newValueList.splice( + Math.min(truthyValueStartIndex, month), + lengthDiff, + ...Array(lengthDiff + 1).fill(true), + ); + const maxIndex = Math.max(truthyValueStartIndex, month) + 1; + const remaining = newValueList.length - maxIndex; + // Fill remaining trailing value with false + newValueList.splice( + maxIndex, + remaining, + ...Array(remaining).fill(false), + ); + // Make sure that yearly average is always false when selecting a range + newValueList.splice(12, 1, false); + + return listToMap( + newValueList, + (_, key) => key, + (currentValue) => currentValue, + ); + }, + name, + ); + }, + [onChange, name], + ); + + return ( +
+
+ {monthKeyList.map( + (key) => { + const date = new Date(); + date.setDate(1); + date.setMonth(key); + date.setHours(0, 0, 0, 0); + + const monthName = date.toLocaleString( + navigator.language, + { month: 'short' }, + ); + + return ( + + + {monthName} + + + + + + + + ); + }, + )} +
+
+ + + Yearly Average + + +
+ ); +} + +export default MultiMonthSelectInput; diff --git a/app/views/Preparedness/RiskAnalysis/MultiMonthSelectInput/styles.module.css b/app/views/Preparedness/RiskAnalysis/MultiMonthSelectInput/styles.module.css new file mode 100644 index 0000000..d1e09f9 --- /dev/null +++ b/app/views/Preparedness/RiskAnalysis/MultiMonthSelectInput/styles.module.css @@ -0,0 +1,129 @@ +.multi-month-select-input { + display: flex; + align-items: flex-start; + gap: var(--go-ui-spacing-md); + + .month { + display: flex; + align-items: stretch; + flex-direction: column; + gap: var(--go-ui-spacing-md); + + --circle-size: 0.8rem; + + .name { + transition: var(--go-ui-duration-transition-medium) ease-in-out transform; + font-weight: var(--go-ui-font-weight-medium); + } + + .visual-que { + display: flex; + align-items: center; + height: var(--circle-size); + + .end-border, + .start-border { + flex-grow: 1; + background-color: var(--go-ui-color-separator); + height: var(--go-ui-width-separator-sm); + } + + .circle { + transition: var(--go-ui-duration-transition-medium) ease-in-out all; + border-radius: 50%; + background-color: var(--go-ui-color-separator); + width: var(--go-ui-width-separator-sm); + height: 100%; + } + } + + &:first-child { + .visual-que { + .start-border { + background-color: transparent; + } + } + } + + &:last-child { + .visual-que { + .end-border { + background-color: transparent; + } + } + } + + &.active { + color: var(--go-ui-color-primary-red); + + .visual-que { + .circle { + background-color: var(--go-ui-color-primary-red); + height: var(--go-ui-width-separator-sm); + } + } + + &.active ~ .active { + .start-border { + background-color: var(--go-ui-color-primary-red); + } + } + + &:not(:nth-last-child(1 of .active)) { + .end-border { + background-color: var(--go-ui-color-primary-red); + } + } + + &:not(.active ~ .active) { + .visual-que { + .circle { + width: var(--circle-size); + height: 100%; + } + } + } + + &:nth-last-child(1 of .active) { + .visual-que { + .circle { + width: var(--circle-size); + height: 100%; + } + } + } + } + + } + + .month-list { + display: flex; + flex-grow: 1; + + .month { + flex-grow: 1; + } + } + + .separator { + background-color: var(--go-ui-color-separator); + width: var(--go-ui-width-separator-sm); + height: 1.5rem; + } + + @media screen and (width <= 40rem) { + flex-wrap: wrap; + + .separator { + display: none; + } + + .month-list { + .month { + .name { + transform: rotate(-45deg); + } + } + } + } +} diff --git a/app/views/Preparedness/RiskAnalysis/PossibleEarlyActionTable/index.tsx b/app/views/Preparedness/RiskAnalysis/PossibleEarlyActionTable/index.tsx new file mode 100644 index 0000000..ccacd3b --- /dev/null +++ b/app/views/Preparedness/RiskAnalysis/PossibleEarlyActionTable/index.tsx @@ -0,0 +1,189 @@ +import { + Container, + Pager, + SelectInput, + Table, +} from '@ifrc-go/ui'; +import { + createStringColumn, + numericIdSelector, + stringKeySelector, + stringNameSelector, + stringValueSelector, +} from '@ifrc-go/ui/utils'; +import { + isDefined, + isNotDefined, +} from '@togglecorp/fujs'; + +import type { components as riskApiComponents } from '#generated/riskTypes'; +import useFilterState from '#hooks/useFilterState'; +import type { + GoApiResponse, + RiskApiResponse, +} from '#utils/restRequest'; +import { useRiskRequest } from '#utils/restRequest'; + +import styles from './styles.module.css'; + +type HazardType = riskApiComponents['schemas']['CommonHazardTypeEnumKey']; + +type PossibleEarlyActionsResponse = RiskApiResponse<'/api/v1/early-actions/'>; +type ResponseItem = NonNullable[number]; +type CountryResponse = GoApiResponse<'/api/v2/country/{id}/'>; + +interface Props { + countryId: number; + countryResponse: CountryResponse | undefined; +} + +function PossibleEarlyActionTable(props: Props) { + const { + countryId, + countryResponse, + } = props; + const { + page, + setPage, + rawFilter, + filter, + filtered, + setFilterField, + limit, + offset, + } = useFilterState<{ + // FIXME hazardType should be HazardType + // hazardType?: HazardType, + hazardType?: string, + sector?: string, + }>({ + filter: {}, + pageSize: 5, + }); + + const columns = [ + createStringColumn( + 'hazard_type_display', + 'Hazard', + (item) => item?.hazard_type_display, + ), + createStringColumn( + 'early_actions', + 'Possible Early Actions', + (item) => item?.early_actions, + ), + createStringColumn( + 'location', + 'Location', + (item) => item?.location, + ), + createStringColumn( + 'sector', + 'Sector', + (item) => item?.sectors_details?.map((d) => d.name).join(', '), + ), + createStringColumn( + 'intended_purpose', + 'Intended Purpose', + (item) => item?.intended_purpose, + ), + createStringColumn( + 'organization', + 'Organisation', + (item) => item?.organization, + ), + createStringColumn( + 'implementation_date_raw', + 'Implementation Date', + (item) => item?.implementation_date_raw, + ), + createStringColumn( + 'impact_actions', + 'Impact/Action', + (item) => item?.impact_action, + ), + createStringColumn( + 'evidence_of_success', + 'Evidence of Success', + (item) => item?.evidence_of_sucess, // FIXME: fix this typo + ), + ]; + + const { + response: earlyActionsOptionsResponse, + } = useRiskRequest({ + apiType: 'risk', + url: '/api/v1/early-actions/options/', + }); + + const { + pending: pendingPossibleEarlyAction, + response: possibleEarlyActionResponse, + } = useRiskRequest({ + skip: isNotDefined(countryId), + apiType: 'risk', + url: '/api/v1/early-actions/', + query: { + limit, + offset, + iso3: countryResponse?.iso3 ?? undefined, + hazard_type: isDefined(filter.hazardType) + ? [filter.hazardType] as HazardType[] + : undefined, + sectors: filter.sector, + }, + }); + if (!filtered && possibleEarlyActionResponse?.count === 0) { + return null; + } + + return ( + + + + + )} + footerActions={( + + )} + > +
+ + ); +} + +export default PossibleEarlyActionTable; diff --git a/app/views/Preparedness/RiskAnalysis/PossibleEarlyActionTable/styles.module.css b/app/views/Preparedness/RiskAnalysis/PossibleEarlyActionTable/styles.module.css new file mode 100644 index 0000000..1143568 --- /dev/null +++ b/app/views/Preparedness/RiskAnalysis/PossibleEarlyActionTable/styles.module.css @@ -0,0 +1,7 @@ +.possible-early-action-table { + .table { + .cell { + font-size: var(--go-ui-font-size-sm); + } + } +} diff --git a/app/views/Preparedness/RiskAnalysis/ReturnPeriodTable/index.tsx b/app/views/Preparedness/RiskAnalysis/ReturnPeriodTable/index.tsx new file mode 100644 index 0000000..1611bb0 --- /dev/null +++ b/app/views/Preparedness/RiskAnalysis/ReturnPeriodTable/index.tsx @@ -0,0 +1,207 @@ +import { useMemo } from 'react'; +import { + Container, + SelectInput, + Table, + TextOutput, +} from '@ifrc-go/ui'; +import { + createNumberColumn, + createStringColumn, + resolveToComponent, +} from '@ifrc-go/ui/utils'; +import { + isDefined, + isFalsyString, + unique, +} from '@togglecorp/fujs'; + +import Link from '#components/Link'; +import type { + components, + paths, +} from '#generated/riskTypes'; +import useInputState from '#hooks/useInputState'; + +type GetCountryRisk = paths['/api/v1/country-seasonal/']['get']; +type CountryRiskResponse = GetCountryRisk['responses']['200']['content']['application/json']; +type HazardType = components['schemas']['CommonHazardTypeEnumKey']; +interface HazardTypeOption { + hazard_type: HazardType; + hazard_type_display: string; +} +interface TransformedReturnPeriodData { + frequencyDisplay: string; + displacement: number | undefined; + exposure: number | undefined; + economicLosses: number | undefined; +} + +function hazardTypeKeySelector(option: HazardTypeOption) { + return option.hazard_type; +} +function hazardTypeLabelSelector(option: HazardTypeOption) { + return option.hazard_type_display; +} +function returnPeriodKeySelector(option: TransformedReturnPeriodData) { + return option.frequencyDisplay; +} + +interface Props { + data: CountryRiskResponse[number]['return_period_data'] | undefined; +} + +function ReturnPeriodTable(props: Props) { + const { data } = props; + const [hazardType, setHazardType] = useInputState('FL'); + const hazardOptions = useMemo( + () => ( + unique( + data?.map( + (datum) => { + // FIXME: Update isFalsyString to Exclude empty string + // FIXME: Also fix this in server + if (isFalsyString(datum.hazard_type)) { + return undefined; + } + + return { + hazard_type: datum.hazard_type, + hazard_type_display: datum.hazard_type_display, + }; + }, + ).filter(isDefined) ?? [], + (datum) => datum.hazard_type_display, + ) + ), + [data], + ); + + const columns = [ + createStringColumn( + 'frequency', + 'Return Period / Expected Frequency', + (item) => item.frequencyDisplay, + ), + createNumberColumn( + 'numRiskOfDisplacement', + 'People at Risk of Displacement', + (item) => item.displacement, + { + headerInfoTitle: 'People at Risk of Displacement', + headerInfoDescription: resolveToComponent( + 'Figures provided by IDMC from its {source}', + { + source: ( + + Disaster Displacement Risk Model + + ), + }, + ), + maximumFractionDigits: 0, + }, + ), + createNumberColumn( + 'economicLosses', + 'Economic Losses (USD)', + (item) => item.economicLosses, + { + headerInfoTitle: 'Economic Losses (USD)', + headerInfoDescription: resolveToComponent( + 'Figures taken from {source}', + { + source: ( + + World Bank Disaster Risk Country Profiles + + ), + }, + ), + }, + ), + ]; + + const transformedReturnPeriods = useMemo( + () => { + const value = data?.find( + (d) => d.hazard_type === hazardType, + ); + + return [ + { + frequencyDisplay: '1-in-20-year event', + displacement: value?.twenty_years?.population_displacement, + economicLosses: value?.twenty_years?.economic_loss, + exposure: value?.twenty_years?.economic_loss, + }, + { + frequencyDisplay: '1-in-50-year event', + displacement: value?.fifty_years?.population_displacement, + economicLosses: value?.fifty_years?.economic_loss, + exposure: value?.fifty_years?.population_exposure, + }, + { + frequencyDisplay: '1-in-100-year event', + displacement: value?.hundred_years?.population_displacement, + economicLosses: value?.hundred_years?.economic_loss, + exposure: value?.hundred_years?.population_exposure, + }, + { + frequencyDisplay: '1-in-250-year event', + displacement: value?.two_hundred_fifty_years?.population_displacement, + economicLosses: value?.two_hundred_fifty_years?.economic_loss, + exposure: value?.two_hundred_fifty_years?.population_exposure, + }, + { + frequencyDisplay: '1-in-500-year event', + displacement: value?.five_hundred_years?.population_displacement, + economicLosses: value?.five_hundred_years?.economic_loss, + exposure: value?.five_hundred_years?.population_exposure, + }, + ]; + }, + [data, hazardType], + ); + + return ( + + )} + filters={( + + )} + > +
+ + ); +} + +export default ReturnPeriodTable; diff --git a/app/views/Preparedness/RiskAnalysis/RiskBarChart/CombinedChart/index.tsx b/app/views/Preparedness/RiskAnalysis/RiskBarChart/CombinedChart/index.tsx new file mode 100644 index 0000000..e56435c --- /dev/null +++ b/app/views/Preparedness/RiskAnalysis/RiskBarChart/CombinedChart/index.tsx @@ -0,0 +1,327 @@ +import { + Fragment, + useCallback, + useMemo, +} from 'react'; +import { + ChartAxes, + ChartContainer, + NumberOutput, + TextOutput, + Tooltip, +} from '@ifrc-go/ui'; +import { maxSafe } from '@ifrc-go/ui/utils'; +import { + isDefined, + isNotDefined, + listToMap, + mapToMap, +} from '@togglecorp/fujs'; + +import { type components } from '#generated/riskTypes'; +import useTemporalChartData from '#hooks/useTemporalChartData'; +import { + CATEGORY_RISK_HIGH, + CATEGORY_RISK_LOW, + CATEGORY_RISK_MEDIUM, + CATEGORY_RISK_VERY_HIGH, + CATEGORY_RISK_VERY_LOW, +} from '#utils/constants'; +import { type RiskApiResponse } from '#utils/restRequest'; +import { + getDataWithTruthyHazardType, + getFiRiskDataItem, + getWfRiskDataItem, + hazardTypeToColorMap, + monthNumberToNameMap, + type RiskMetricOption, + riskScoreToCategory, +} from '#utils/risk'; + +import styles from './styles.module.css'; + +type CountryRiskResponse = RiskApiResponse<'/api/v1/country-seasonal/'>; +type RiskData = CountryRiskResponse[number]; +type HazardType = components['schemas']['CommonHazardTypeEnumKey']; + +const selectedMonths = { + 0: true, + 1: true, + 2: true, + 3: true, + 4: true, + 5: true, + 6: true, + 7: true, + 8: true, + 9: true, + 10: true, + 11: true, +}; + +const BAR_GAP = 2; + +interface Props { + riskData: RiskData | undefined; + selectedRiskMetricDetail: RiskMetricOption; + selectedHazardType: HazardType | undefined; + hazardListForDisplay: { + hazard_type: HazardType; + hazard_type_display: string; + }[]; +} + +function CombinedChart(props: Props) { + const { + riskData, + selectedHazardType, + selectedRiskMetricDetail, + hazardListForDisplay, + } = props; + + const riskCategoryToLabelMap: Record = useMemo( + () => ({ + [CATEGORY_RISK_VERY_LOW]: 'Very low', + [CATEGORY_RISK_LOW]: 'Low', + [CATEGORY_RISK_MEDIUM]: 'Medium', + [CATEGORY_RISK_HIGH]: 'High', + [CATEGORY_RISK_VERY_HIGH]: 'Very high', + }), + [], + ); + + const fiRiskDataItem = useMemo( + () => getFiRiskDataItem(riskData?.ipc_displacement_data), + [riskData], + ); + const wfRiskDataItem = useMemo( + () => getWfRiskDataItem(riskData?.gwis), + [riskData], + ); + + const selectedRiskData = useMemo( + () => { + if (selectedRiskMetricDetail.key === 'displacement') { + return listToMap( + riskData?.idmc + ?.map(getDataWithTruthyHazardType) + ?.filter(isDefined) ?? [], + (data) => data.hazard_type, + ); + } + + if (selectedRiskMetricDetail.key === 'riskScore') { + const wfEntry = { + ...wfRiskDataItem, + ...mapToMap( + monthNumberToNameMap, + (_, monthName) => monthName, + (monthName) => riskScoreToCategory( + wfRiskDataItem?.[monthName], + 'WF', + ), + ), + }; + return { + ...listToMap( + riskData?.inform_seasonal + ?.map(getDataWithTruthyHazardType) + ?.filter(isDefined) + ?.map((riskItem) => ({ + ...riskItem, + ...mapToMap( + monthNumberToNameMap, + (_, monthName) => monthName, + (monthName) => riskScoreToCategory( + riskItem?.[monthName], + riskItem?.hazard_type, + ), + ), + })) ?? [], + (data) => data.hazard_type, + ), + WF: wfEntry, + }; + } + + const rasterDisplacementData = listToMap( + riskData?.raster_displacement_data + ?.map(getDataWithTruthyHazardType) + ?.filter(isDefined) ?? [], + (datum) => datum.hazard_type, + ); + + if (isNotDefined(fiRiskDataItem)) { + return rasterDisplacementData; + } + + return { + ...rasterDisplacementData, + FI: fiRiskDataItem, + }; + }, + [riskData, selectedRiskMetricDetail, fiRiskDataItem, wfRiskDataItem], + ); + + const filteredRiskData = useMemo( + () => { + if (isNotDefined(selectedHazardType)) { + return selectedRiskData; + } + + const riskDataItem = selectedRiskData[selectedHazardType]; + + return { + [selectedHazardType]: riskDataItem, + }; + }, + [selectedRiskData, selectedHazardType], + ); + + const hazardData = useMemo( + () => { + const monthKeys = Object.keys(selectedMonths) as unknown as ( + keyof typeof selectedMonths + )[]; + + const hazardKeysFromSelectedRisk = Object.keys(filteredRiskData ?? {}) as HazardType[]; + const currentYear = new Date().getFullYear(); + + return ( + monthKeys.map( + (monthKey) => { + const month = monthNumberToNameMap[monthKey]!; + const value = listToMap( + hazardKeysFromSelectedRisk, + (hazardKey) => hazardKey, + (hazardKey) => (filteredRiskData?.[hazardKey]?.[month] ?? 0), + ); + + return { + value, + month, + date: new Date(currentYear, monthKey, 1), + }; + }, + ) + ); + }, + [filteredRiskData], + ); + + const yAxisTickLabelSelector = useCallback( + (value: number) => { + if (selectedRiskMetricDetail.key === 'riskScore') { + return riskCategoryToLabelMap[value]; + } + + return ( + + ); + }, + [riskCategoryToLabelMap, selectedRiskMetricDetail.key], + ); + + const chartData = useTemporalChartData( + hazardData, + { + keySelector: (datum) => datum.date.getTime(), + xValueSelector: (datum) => datum.date, + yValueSelector: (datum) => maxSafe(Object.values(datum.value)), + yAxisTickLabelSelector, + yearlyChart: true, + yValueStartsFromZero: true, + yAxisWidth: selectedRiskMetricDetail.key === 'riskScore' ? 80 : undefined, + yScale: selectedRiskMetricDetail.key === 'riskScore' ? 'linear' : 'cbrt', + yDomain: selectedRiskMetricDetail.key === 'riskScore' ? { min: 0, max: 5 } : undefined, + numYAxisTicks: 6, + }, + ); + + function getChartHeight(y: number) { + return isDefined(y) + ? Math.max( + chartData.dataAreaSize.height - y + chartData.dataAreaOffset.top, + 0, + ) : 0; + } + + const xAxisDiff = chartData.dataAreaSize.width / chartData.numXAxisTicks; + const barGap = Math.min(BAR_GAP, xAxisDiff / 30); + + return ( + + + {chartData.chartPoints.map( + (datum) => ( + + {hazardListForDisplay.map( + ({ hazard_type: hazard, hazard_type_display }, hazardIndex) => { + const value = datum.originalData.value[hazard]; + const y = chartData.yScaleFn(value); + const height = getChartHeight(y); + + const offsetX = barGap; + const numItems = hazardListForDisplay.length; + + const width = Math.max( + + (xAxisDiff / numItems) - offsetX * 2, + 0, + ); + // eslint-disable-next-line max-len + const x = (datum.x - xAxisDiff / 2) + offsetX + (width + barGap) * hazardIndex; + + return ( + + + {datum.originalData.date.toLocaleDateString('default', { month: 'long' })} + {selectedRiskMetricDetail.key === 'riskScore' ? ( + + ) : ( + + )} + + )} + /> + + ); + }, + )} + + ), + )} + + ); +} + +export default CombinedChart; diff --git a/app/views/Preparedness/RiskAnalysis/RiskBarChart/CombinedChart/styles.module.css b/app/views/Preparedness/RiskAnalysis/RiskBarChart/CombinedChart/styles.module.css new file mode 100644 index 0000000..87bbedb --- /dev/null +++ b/app/views/Preparedness/RiskAnalysis/RiskBarChart/CombinedChart/styles.module.css @@ -0,0 +1,4 @@ +.combined-chart { + height: 20rem; +} + diff --git a/app/views/Preparedness/RiskAnalysis/RiskBarChart/FoodInsecurityChart/FiChartPoint/index.tsx b/app/views/Preparedness/RiskAnalysis/RiskBarChart/FoodInsecurityChart/FiChartPoint/index.tsx new file mode 100644 index 0000000..108fdbf --- /dev/null +++ b/app/views/Preparedness/RiskAnalysis/RiskBarChart/FoodInsecurityChart/FiChartPoint/index.tsx @@ -0,0 +1,93 @@ +import { useMemo } from 'react'; +import { + ChartPoint, + TextOutput, + Tooltip, +} from '@ifrc-go/ui'; +import { isDefined } from '@togglecorp/fujs'; + +const currentYear = new Date().getFullYear(); + +interface Props { + className?: string; + dataPoint: { + originalData: { + year?: number; + month: number; + analysis_date?: string; + total_displacement: number; + }, + key: number | string; + x: number; + y: number; + }; +} + +function FiChartPoint(props: Props) { + const { + dataPoint: { + x, + y, + originalData, + }, + className, + } = props; + + const title = useMemo( + () => { + const { + year, + month, + } = originalData; + + if (isDefined(year)) { + return new Date(year, month - 1, 1).toLocaleString( + navigator.language, + { + year: 'numeric', + month: 'long', + }, + ); + } + + const formattedMonth = new Date(currentYear, month - 1, 1).toLocaleString( + navigator.language, + { month: 'long' }, + ); + + return `Average for ${formattedMonth}`; + }, + [originalData], + ); + + return ( + + + {isDefined(originalData.analysis_date) && ( + + )} + + + )} + /> + + ); +} + +export default FiChartPoint; diff --git a/app/views/Preparedness/RiskAnalysis/RiskBarChart/FoodInsecurityChart/index.tsx b/app/views/Preparedness/RiskAnalysis/RiskBarChart/FoodInsecurityChart/index.tsx new file mode 100644 index 0000000..c0dac56 --- /dev/null +++ b/app/views/Preparedness/RiskAnalysis/RiskBarChart/FoodInsecurityChart/index.tsx @@ -0,0 +1,224 @@ +import { useMemo } from 'react'; +import { + ChartAxes, + ChartContainer, +} from '@ifrc-go/ui'; +import { + avgSafe, + getDiscretePathDataList, +} from '@ifrc-go/ui/utils'; +import { + isDefined, + isNotDefined, + listToGroupList, + mapToList, +} from '@togglecorp/fujs'; + +import useTemporalChartData from '#hooks/useTemporalChartData'; +import { type RiskApiResponse } from '#utils/restRequest'; +import { getPrioritizedIpcData } from '#utils/risk'; + +import FiChartPoint from './FiChartPoint'; + +import styles from './styles.module.css'; + +type CountryRiskResponse = RiskApiResponse<'/api/v1/country-seasonal/'>; +type RiskData = CountryRiskResponse[number]; + +const colors = [ + 'var(--go-ui-color-gray-30)', + 'var(--go-ui-color-gray-40)', + 'var(--go-ui-color-gray-50)', + 'var(--go-ui-color-gray-60)', + 'var(--go-ui-color-gray-70)', + 'var(--go-ui-color-gray-80)', + 'var(--go-ui-color-gray-90)', +]; + +interface Props { + ipcData: RiskData['ipc_displacement_data'] | undefined; + showHistoricalData?: boolean; + showProjection?: boolean; +} + +function FoodInsecurityChart(props: Props) { + const { + ipcData, + showHistoricalData, + showProjection, + } = props; + + const uniqueData = useMemo( + () => getPrioritizedIpcData(ipcData ?? []), + [ipcData], + ); + + const chartData = useTemporalChartData( + uniqueData, + { + keySelector: (datum) => datum.id, + xValueSelector: (datum) => new Date(datum.year, datum.month - 1, 1), + yValueSelector: (datum) => datum.total_displacement, + yValueStartsFromZero: true, + yearlyChart: true, + }, + ); + + const latestProjectionYear = useMemo( + () => { + const projectionData = uniqueData.filter( + (fiData) => fiData.estimation_type !== 'current', + ).map( + (fiData) => fiData.year, + ); + + return Math.max(...projectionData); + }, + [uniqueData], + ); + + const historicalPointsDataList = useMemo( + () => { + const yearGroupedDataPoints = listToGroupList( + chartData.chartPoints.filter( + (pathPoints) => pathPoints.originalData.year !== latestProjectionYear, + ), + (dataPoint) => dataPoint.originalData.year, + ); + + return mapToList( + yearGroupedDataPoints, + (list, key) => ({ + key, + list, + }), + ); + }, + [latestProjectionYear, chartData.chartPoints], + ); + + const averagePointsData = useMemo( + () => { + const monthGroupedDataPoints = listToGroupList( + chartData.chartPoints, + (dataPoint) => dataPoint.originalData.month, + ); + + return mapToList( + monthGroupedDataPoints, + (list, month) => { + const averageDisplacement = avgSafe( + list.map( + (fiData) => fiData.originalData.total_displacement, + ), + ); + + if (isNotDefined(averageDisplacement)) { + return undefined; + } + + return { + key: month, + x: list[0]!.x, + y: chartData.yScaleFn(averageDisplacement), + originalData: { + total_displacement: averageDisplacement, + month: Number(month), + }, + }; + }, + ).filter(isDefined); + }, + [chartData], + ); + + const predictionPointsData = useMemo( + () => ( + chartData.chartPoints.filter( + (pathPoints) => pathPoints.originalData.year === latestProjectionYear, + ) + ), + [chartData.chartPoints, latestProjectionYear], + ); + + return ( + + + {showHistoricalData && historicalPointsDataList.map( + (historicalPointsData, i) => ( + + {getDiscretePathDataList(historicalPointsData.list).map( + (discretePath) => ( + + ), + )} + {historicalPointsData.list.map( + (pointData) => ( + + ), + )} + + ), + )} + {showProjection && ( + + {getDiscretePathDataList(predictionPointsData).map( + (discretePath) => ( + + ), + )} + {predictionPointsData.map( + (pointData) => ( + + ), + )} + + )} + + {getDiscretePathDataList(averagePointsData).map( + (discretePath) => ( + + ), + )} + {averagePointsData.map( + (pointData) => ( + + ), + )} + + + ); +} + +export default FoodInsecurityChart; diff --git a/app/views/Preparedness/RiskAnalysis/RiskBarChart/FoodInsecurityChart/styles.module.css b/app/views/Preparedness/RiskAnalysis/RiskBarChart/FoodInsecurityChart/styles.module.css new file mode 100644 index 0000000..89fa263 --- /dev/null +++ b/app/views/Preparedness/RiskAnalysis/RiskBarChart/FoodInsecurityChart/styles.module.css @@ -0,0 +1,29 @@ +.food-insecurity-chart { + height: 20rem; + + .point { + color: currentcolor; + } + + .path { + fill: none; + stroke-width: 2; + stroke: currentcolor; + } + + .historical-data { + opacity: 0.6; + + &:hover { + opacity: 1; + } + } + + .prediction { + color: var(--go-ui-color-primary-red); + } + + .average { + color: var(--go-ui-color-hazard-fi); + } +} diff --git a/app/views/Preparedness/RiskAnalysis/RiskBarChart/WildfireChart/index.tsx b/app/views/Preparedness/RiskAnalysis/RiskBarChart/WildfireChart/index.tsx new file mode 100644 index 0000000..43b6671 --- /dev/null +++ b/app/views/Preparedness/RiskAnalysis/RiskBarChart/WildfireChart/index.tsx @@ -0,0 +1,247 @@ +import { + useCallback, + useMemo, + useState, +} from 'react'; +import { + ChartAxes, + ChartContainer, + ChartPoint, + TextOutput, + Tooltip, +} from '@ifrc-go/ui'; +import { + avgSafe, + formatNumber, + getDiscretePathDataList, + getPathData, + resolveToString, +} from '@ifrc-go/ui/utils'; +import { + isDefined, + isNotDefined, + listToGroupList, + mapToList, +} from '@togglecorp/fujs'; + +import { type paths } from '#generated/riskTypes'; +import useTemporalChartData from '#hooks/useTemporalChartData'; +import { + COLOR_PRIMARY_BLUE, + DEFAULT_Y_AXIS_WIDTH_WITH_LABEL, +} from '#utils/constants'; + +import styles from './styles.module.css'; + +type GetCountryRisk = paths['/api/v1/country-seasonal/']['get']; +type CountryRiskResponse = GetCountryRisk['responses']['200']['content']['application/json']; +type RiskData = CountryRiskResponse[number]; + +interface ChartPoint { + x: number; + y: number; + label: string | undefined; +} + +interface Props { + gwisData: RiskData['gwis'] | undefined; +} + +const currentYear = new Date().getFullYear(); + +function WildfireChart(props: Props) { + const { gwisData } = props; + + const aggregatedList = useMemo( + () => { + const monthGroupedData = listToGroupList( + gwisData?.filter((dataItem) => dataItem.dsr_type === 'monthly') ?? [], + (gwisItem) => gwisItem.month, + ); + + return mapToList( + monthGroupedData, + (monthlyData, monthKey) => { + const average = avgSafe(monthlyData.map((dataItem) => dataItem.dsr)) ?? 0; + const min = avgSafe(monthlyData.map((dataItem) => dataItem.dsr_min)) ?? 0; + const max = avgSafe(monthlyData.map((dataItem) => dataItem.dsr_max)) ?? 0; + + const current = monthlyData.find( + (dataItem) => dataItem.year === currentYear, + )?.dsr; + + const month = Number(monthKey) - 1; + + return { + date: new Date(currentYear, month, 1), + month, + min, + max, + average, + current, + maxValue: Math.max(min, max, average, current ?? 0), + }; + }, + ); + }, + [gwisData], + ); + + const chartData = useTemporalChartData( + aggregatedList, + { + keySelector: (datum) => datum.month, + xValueSelector: (datum) => datum.date, + yValueSelector: (datum) => datum.maxValue, + yearlyChart: true, + yAxisWidth: DEFAULT_Y_AXIS_WIDTH_WITH_LABEL, + yValueStartsFromZero: true, + }, + ); + + const minPoints = chartData.chartPoints.map( + (dataPoint) => ({ + ...dataPoint, + y: chartData.yScaleFn(dataPoint.originalData.min), + }), + ); + + const maxPoints = chartData.chartPoints.map( + (dataPoint) => ({ + ...dataPoint, + y: chartData.yScaleFn(dataPoint.originalData.max), + }), + ); + + const minMaxPoints = [...minPoints, ...[...maxPoints].reverse()]; + + const currentYearPoints = chartData.chartPoints.map( + (dataPoint) => { + if (isNotDefined(dataPoint.originalData.current)) { + return undefined; + } + + return { + ...dataPoint, + y: chartData.yScaleFn(dataPoint.originalData.current), + }; + }, + ).filter(isDefined); + + const averagePoints = chartData.chartPoints.map( + (dataPoint) => ({ + ...dataPoint, + y: chartData.yScaleFn(dataPoint.originalData.average), + }), + ); + + const tooltipSelector = ( + (_: number | string, i: number) => { + const date = new Date(currentYear, i, 1); + const monthData = aggregatedList[i]!; + + return ( + + + + + + )} + /> + ); + } + ); + + const [hoveredAxisIndex, setHoveredAxisIndex] = useState(); + + const handleHover = useCallback( + (_: number | string | undefined, i: number | undefined) => { + setHoveredAxisIndex(i); + }, + [], + ); + + return ( + + + + {getDiscretePathDataList(currentYearPoints).map( + (points) => ( + + ), + )} + {currentYearPoints.map( + (pointData, i) => ( + + ), + )} + + + + {averagePoints.map( + (pointData, i) => ( + + ), + )} + + + + ); +} + +export default WildfireChart; diff --git a/app/views/Preparedness/RiskAnalysis/RiskBarChart/WildfireChart/styles.module.css b/app/views/Preparedness/RiskAnalysis/RiskBarChart/WildfireChart/styles.module.css new file mode 100644 index 0000000..26d2171 --- /dev/null +++ b/app/views/Preparedness/RiskAnalysis/RiskBarChart/WildfireChart/styles.module.css @@ -0,0 +1,25 @@ +.wildfire-chart { + height: 20rem; + + .min-max-path { + fill: var(--go-ui-color-gray-20); + } + + .path { + fill: none; + stroke: currentcolor; + stroke-width: 2; + } + + .point { + color: currentcolor; + } + + .average { + color: var(--go-ui-color-primary-blue); + } + + .current-year { + color: var(--go-ui-color-primary-red); + } +} diff --git a/app/views/Preparedness/RiskAnalysis/RiskBarChart/index.tsx b/app/views/Preparedness/RiskAnalysis/RiskBarChart/index.tsx new file mode 100644 index 0000000..7453c8b --- /dev/null +++ b/app/views/Preparedness/RiskAnalysis/RiskBarChart/index.tsx @@ -0,0 +1,375 @@ +import { + useCallback, + useMemo, +} from 'react'; +import { + Checkbox, + Container, + LegendItem, + SelectInput, +} from '@ifrc-go/ui'; +import { + resolveToString, + stringLabelSelector, +} from '@ifrc-go/ui/utils'; +import { + isDefined, + isFalsyString, + isNotDefined, + listToGroupList, + listToMap, + unique, +} from '@togglecorp/fujs'; + +import useInputState from '#hooks/useInputState'; +import { + COLOR_LIGHT_GREY, + COLOR_PRIMARY_BLUE, + COLOR_PRIMARY_RED, +} from '#utils/constants'; +import { type RiskApiResponse } from '#utils/restRequest'; +import type { + HazardType, + RiskMetric, + RiskMetricOption, +} from '#utils/risk'; +import { + applicableHazardsByRiskMetric, + getDataWithTruthyHazardType, + getFiRiskDataItem, + hasSomeDefinedValue, + hazardTypeKeySelector, + hazardTypeLabelSelector, + hazardTypeToColorMap, + riskMetricKeySelector, +} from '#utils/risk'; + +import CombinedChart from './CombinedChart'; +import FoodInsecurityChart from './FoodInsecurityChart'; +import WildfireChart from './WildfireChart'; + +import styles from './styles.module.css'; + +type CountryRiskResponse = RiskApiResponse<'/api/v1/country-seasonal/'>; +type RiskData = CountryRiskResponse[number]; + +const currentYear = new Date().getFullYear(); + +interface Props { + pending: boolean; + seasonalRiskData: RiskData | undefined; +} + +function RiskBarChart(props: Props) { + const { + seasonalRiskData, + pending, + } = props; + + const [selectedRiskMetric, setSelectedRiskMetric] = useInputState('exposure'); + const [ + selectedHazardType, + setSelectedHazardType, + ] = useInputState(undefined); + const [showFiHistoricalData, setShowFiHistoricalData] = useInputState(false); + const [showFiProjection, setShowFiProjection] = useInputState(false); + + const handleRiskMetricChange = useCallback( + (riskMetric: RiskMetric) => { + setSelectedRiskMetric(riskMetric); + setSelectedHazardType(undefined); + }, + [setSelectedHazardType, setSelectedRiskMetric], + ); + + const riskMetricOptions: RiskMetricOption[] = useMemo( + () => ([ + { + key: 'exposure', + label: 'People Exposed', + applicableHazards: applicableHazardsByRiskMetric.exposure, + }, + { + key: 'displacement', + label: 'People at Risk of Displacement', + applicableHazards: applicableHazardsByRiskMetric.displacement, + }, + { + key: 'riskScore', + label: 'Risk Score', + applicableHazards: applicableHazardsByRiskMetric.riskScore, + }, + ]), + [], + ); + + const selectedRiskMetricDetail = useMemo( + () => riskMetricOptions.find( + (option) => option.key === selectedRiskMetric, + ) ?? riskMetricOptions[0]!, + [selectedRiskMetric, riskMetricOptions], + ); + + const data = useMemo( + () => { + if (isNotDefined(seasonalRiskData)) { + return undefined; + } + + const { + idmc, + ipc_displacement_data, + raster_displacement_data, + gwis_seasonal, + inform_seasonal, + } = seasonalRiskData; + + const displacement = idmc?.map( + (dataItem) => { + if (!hasSomeDefinedValue(dataItem)) { + return undefined; + } + + return getDataWithTruthyHazardType(dataItem); + }, + ).filter(isDefined) ?? []; + + const groupedIpc = Object.values( + listToGroupList( + ipc_displacement_data ?? [], + (ipcDataItem) => ipcDataItem.country, + ), + ); + + const exposure = [ + ...raster_displacement_data?.map( + (dataItem) => { + if (!hasSomeDefinedValue(dataItem)) { + return undefined; + } + + return getDataWithTruthyHazardType(dataItem); + }, + ) ?? [], + ...groupedIpc.map(getFiRiskDataItem), + ].filter(isDefined); + + const riskScore = unique( + [ + ...inform_seasonal?.map( + (dataItem) => { + if (!hasSomeDefinedValue(dataItem)) { + return undefined; + } + + return getDataWithTruthyHazardType(dataItem); + }, + ) ?? [], + ...gwis_seasonal?.map( + (dataItem) => { + if (!hasSomeDefinedValue(dataItem)) { + return undefined; + } + + return getDataWithTruthyHazardType(dataItem); + }, + ) ?? [], + ].filter(isDefined), + (item) => `${item.country_details.iso3}-${item.hazard_type}`, + ); + + return { + displacement, + exposure, + riskScore, + }; + }, + [seasonalRiskData], + ); + + const availableHazards: { [key in HazardType]?: string } | undefined = useMemo( + () => { + if (isNotDefined(data)) { + return undefined; + } + + if (selectedRiskMetric === 'exposure') { + return { + ...listToMap( + data.exposure, + (item) => item.hazard_type, + (item) => item.hazard_type_display, + ), + }; + } + + if (selectedRiskMetric === 'displacement') { + return { + ...listToMap( + data.displacement, + (item) => item.hazard_type, + (item) => item.hazard_type_display, + ), + }; + } + + if (selectedRiskMetric === 'riskScore') { + return { + ...listToMap( + data.riskScore, + (item) => item.hazard_type, + (item) => item.hazard_type_display, + ), + }; + } + + return undefined; + }, + [data, selectedRiskMetric], + ); + + const hazardTypeOptions = useMemo( + () => ( + selectedRiskMetricDetail.applicableHazards.map( + (hazardType) => { + const hazard_type_display = availableHazards?.[hazardType]; + if (isFalsyString(hazard_type_display)) { + return undefined; + } + + return { + hazard_type: hazardType, + hazard_type_display, + }; + }, + ).filter(isDefined) + ), + [availableHazards, selectedRiskMetricDetail], + ); + + const hazardListForDisplay = useMemo( + () => { + if (isNotDefined(selectedHazardType)) { + return hazardTypeOptions; + } + + return hazardTypeOptions.filter( + (hazardType) => hazardType.hazard_type === selectedHazardType, + ); + }, + [selectedHazardType, hazardTypeOptions], + ); + + return ( + } + filters={( + <> + + + {selectedHazardType === 'FI' && ( +
+ + +
+ )} + + )} + pending={pending} + > + {selectedHazardType === 'FI' && ( + + )} + {selectedHazardType === 'WF' && ( + + )} + {selectedHazardType !== 'FI' && selectedHazardType !== 'WF' && ( + + )} +
+ {hazardListForDisplay.map( + (hazard) => ( + + ), + )} + {selectedHazardType === 'WF' && ( + <> + + + + + )} +
+
+ ); +} + +export default RiskBarChart; diff --git a/app/views/Preparedness/RiskAnalysis/RiskBarChart/styles.module.css b/app/views/Preparedness/RiskAnalysis/RiskBarChart/styles.module.css new file mode 100644 index 0000000..b978145 --- /dev/null +++ b/app/views/Preparedness/RiskAnalysis/RiskBarChart/styles.module.css @@ -0,0 +1,20 @@ +.risk-bar-chart { + .fi-filters { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: var(--go-ui-spacing-md); + grid-column: span 2; + + @media screen and (width <= 40rem) { + grid-column: unset; + } + } + + .legend { + display: flex; + gap: var(--go-ui-spacing-md); + background-color: var(--go-ui-color-background); + padding: var(--go-ui-spacing-sm) var(--go-ui-spacing-md); + } +} diff --git a/app/views/Preparedness/RiskAnalysis/RiskImminentEventMap/LayerOptions/index.tsx b/app/views/Preparedness/RiskAnalysis/RiskImminentEventMap/LayerOptions/index.tsx new file mode 100644 index 0000000..ce7496d --- /dev/null +++ b/app/views/Preparedness/RiskAnalysis/RiskImminentEventMap/LayerOptions/index.tsx @@ -0,0 +1,146 @@ +import { useMemo } from 'react'; +import { + Legend, + ListView, + Switch, +} from '@ifrc-go/ui'; + +import useSetFieldValue from '#hooks/useSetFieldValue'; +import { + COLOR_DARK_GREY, + COLOR_GREEN, + COLOR_ORANGE, + COLOR_RED, +} from '#utils/constants'; + +import { type RiskLayerSeverity } from '../utils'; + +export interface LayerOptionsValue { + showStormPosition: boolean; + showForecastUncertainty: boolean; + showTrackLine: boolean; + showExposedArea: boolean; +} + +interface SeverityLegendItem { + severity: RiskLayerSeverity; + label: string; + color: string; +} + +function severitySelector(item: SeverityLegendItem) { + return item.severity; +} +function labelSelector(item: SeverityLegendItem) { + return item.label; +} +function colorSelector(item: SeverityLegendItem) { + return item.color; +} + +interface Props { + value: LayerOptionsValue; + onChange: React.Dispatch>; + + exposureAreaControlHidden?: boolean; +} + +function LayerOptions(props: Props) { + const { + exposureAreaControlHidden, + value, + onChange, + } = props; + + const setFieldValue = useSetFieldValue(onChange); + + // FIXME: use strings + // FIXME: These are hard-coded for Gdacs source. + // Currently we are only showing severity control for Gdacs + const severityLegendItems = useMemo(() => ([ + { + severity: 'green', + label: '60 km/h', + color: COLOR_GREEN, + }, + { + severity: 'orange', + label: '90 km/h', + color: COLOR_ORANGE, + }, + { + severity: 'red', + label: '120 km/h', + color: COLOR_RED, + }, + { + severity: 'unknown', + label: 'Unknown', + color: COLOR_DARK_GREY, + }, + ]), []); + + return ( + + + {!exposureAreaControlHidden && ( + + + {value.showExposedArea && ( + + )} + + )} + + + + ); +} + +export default LayerOptions; diff --git a/app/views/Preparedness/RiskAnalysis/RiskImminentEventMap/index.tsx b/app/views/Preparedness/RiskAnalysis/RiskImminentEventMap/index.tsx new file mode 100644 index 0000000..6541868 --- /dev/null +++ b/app/views/Preparedness/RiskAnalysis/RiskImminentEventMap/index.tsx @@ -0,0 +1,505 @@ +import { + useCallback, + useMemo, + useState, +} from 'react'; +import { + Container, + ListView, + RawList, +} from '@ifrc-go/ui'; +import { + isDefined, + isNotDefined, + mapToList, +} from '@togglecorp/fujs'; +import { + getLayerName, + MapBounds, + MapImage, + MapLayer, + MapOrder, + MapSource, + MapState, +} from '@togglecorp/re-map'; +import getBuffer from '@turf/buffer'; +import type { + LngLatBoundsLike, + SymbolLayer, +} from 'mapbox-gl'; + +import GlobalMap from '#components/GlobalMap'; +import GoMapContainer from '#components/GoMapContainer'; +import { type components } from '#generated/riskTypes'; +import useDebouncedValue from '#hooks/useDebouncedValue'; +import { + COLOR_WHITE, + DEFAULT_MAP_PADDING, + DURATION_MAP_ZOOM, +} from '#utils/constants'; +import { getGeoJsonBounds } from '#utils/geo'; + +import LayerOptions, { type LayerOptionsValue } from './LayerOptions'; +import { + activeHazardPointLayer, + exposureFillLayer, + exposureFillOutlineLayer, + geojsonSourceOptions, + hazardKeyToIconMap, + hazardPointIconLayout, + hazardPointLayer, + invisibleCircleLayer, + invisibleFillLayer, + invisibleLayout, + invisibleLineLayer, + trackLineLayer, + trackPointLayer, + trackPointOuterCircleLayer, + uncertaintyConeLayer, +} from './mapStyles'; +import { type RiskLayerProperties } from './utils'; + +import styles from './styles.module.css'; + +const mapImageOption = { + sdf: true, +}; + +type CommonHazardType = components['schemas']['CommonHazardTypeEnumKey']; + +const hazardKeys = Object.keys(hazardKeyToIconMap) as CommonHazardType[]; + +const mapIcons = mapToList( + hazardKeyToIconMap, + (icon, key) => (icon ? ({ key, icon }) : undefined), +).filter(isDefined); + +type EventPointProperties = { + id: string | number, + hazard_type: CommonHazardType, +} + +export type EventPointFeature = GeoJSON.Feature; + +export interface RiskEventListItemProps { + data: EVENT; + expanded: boolean; + onExpandClick: (eventId: number | string) => void; + className?: string; + children?: React.ReactNode; +} + +export interface RiskEventDetailProps { + data: EVENT; + exposure: EXPOSURE | undefined; + pending: boolean; + children?: React.ReactNode; +} + +type Footprint = GeoJSON.FeatureCollection | undefined; + +// FIXME: read this from common type +type ImminentEventSource = 'pdc' | 'wfpAdam' | 'gdacs' | 'meteoSwiss'; + +interface Props { + // FIXME: use props for configuration rather than + // passing source here + source: ImminentEventSource; + events: EVENT[] | undefined; + keySelector: (event: EVENT) => KEY; + hazardTypeSelector: (event: EVENT) => CommonHazardType | '' | undefined; + pointFeatureSelector: (event: EVENT) => EventPointFeature | undefined; + footprintSelector: (activeEventExposure: EXPOSURE | undefined) => Footprint | undefined; + activeEventExposure: EXPOSURE | undefined; + listItemRenderer: React.ComponentType>; + detailRenderer: React.ComponentType>; + pending: boolean; + sidePanelHeading: React.ReactNode; + bbox: LngLatBoundsLike | undefined; + onActiveEventChange: (eventId: KEY | undefined) => void; + activeEventExposurePending: boolean; +} + +function RiskImminentEventMap< + EVENT, + EXPOSURE, + KEY extends string | number +>(props: Props) { + const { + events, + pointFeatureSelector, + keySelector, + listItemRenderer, + detailRenderer, + pending, + activeEventExposure, + hazardTypeSelector, + footprintSelector, + sidePanelHeading, + bbox, + onActiveEventChange, + activeEventExposurePending, + source, + } = props; + + const [activeEventId, setActiveEventId] = useState(undefined); + const [layerOptions, setLayerOptions] = useState({ + showStormPosition: true, + showForecastUncertainty: true, + showTrackLine: true, + showExposedArea: true, + }); + const activeEvent = useMemo( + () => { + if (isNotDefined(activeEventId)) { + return undefined; + } + + return events?.find( + (event) => keySelector(event) === activeEventId, + ); + }, + [activeEventId, keySelector, events], + ); + + const eventVisibilityAttributes = useMemo( + () => events?.map((event) => { + const key = keySelector(event); + + return { + id: key, + value: isNotDefined(activeEventId) || activeEventId === key, + }; + }), + [events, activeEventId, keySelector], + ); + + const activeEventFootprint = useMemo( + () => { + if (isNotDefined(activeEventId) || activeEventExposurePending) { + return undefined; + } + + return footprintSelector(activeEventExposure); + }, + [activeEventId, activeEventExposure, activeEventExposurePending, footprintSelector], + ); + + const bounds = useMemo( + () => { + if (isNotDefined(activeEvent) || activeEventExposurePending) { + return bbox; + } + + const activePoint = pointFeatureSelector(activeEvent); + if (isNotDefined(activePoint)) { + return bbox; + } + + const bufferedPoint = getBuffer(activePoint, 10); + + if (isNotDefined(bufferedPoint)) { + return bbox; + } + + if (activeEventFootprint) { + return getGeoJsonBounds({ + ...activeEventFootprint, + features: [ + ...activeEventFootprint.features, + bufferedPoint, + ], + }); + } + + return getGeoJsonBounds(bufferedPoint); + }, + [activeEvent, activeEventFootprint, pointFeatureSelector, bbox, activeEventExposurePending], + ); + + // Avoid abrupt zooming + const boundsSafe = useDebouncedValue(bounds); + + const pointFeatureCollection = useMemo< + GeoJSON.FeatureCollection + >( + () => ({ + type: 'FeatureCollection' as const, + features: events?.map( + (event) => { + const feature = pointFeatureSelector(event); + + if (isNotDefined(feature)) { + return undefined; + } + + return { + ...feature, + id: keySelector(event), + }; + }, + ).filter(isDefined) ?? [], + }), + [events, pointFeatureSelector, keySelector], + ); + + const setActiveEventIdSafe = useCallback( + (eventId: string | number | undefined) => { + const eventIdSafe = eventId as KEY | undefined; + + if (activeEventId === eventIdSafe) { + setActiveEventId(undefined); + onActiveEventChange(undefined); + } else { + setActiveEventId(eventIdSafe); + onActiveEventChange(eventIdSafe); + } + }, + [onActiveEventChange, activeEventId], + ); + + const handlePointClick = useCallback( + (e: mapboxgl.MapboxGeoJSONFeature) => { + const pointProperties = e.properties as EventPointProperties; + setActiveEventIdSafe(pointProperties.id as KEY | undefined); + return undefined; + }, + [setActiveEventIdSafe], + ); + + const DetailComponent = detailRenderer; + + const eventListRendererParams = useCallback( + (_: string | number, event: EVENT): RiskEventListItemProps => ({ + data: event, + onExpandClick: setActiveEventIdSafe, + expanded: activeEventId === keySelector(event), + className: styles.riskEventListItem, + children: activeEventId === keySelector(event) && ( + + {hazardTypeSelector(event) === 'TC' && ( + + )} + + ), + }), + [ + setActiveEventIdSafe, + activeEventExposure, + activeEventExposurePending, + layerOptions, + hazardTypeSelector, + DetailComponent, + activeEventId, + keySelector, + source, + ], + ); + + const [loadedIcons, setLoadedIcons] = useState>({}); + + const handleIconLoad = useCallback( + (loaded: boolean, key: CommonHazardType) => { + setLoadedIcons((prevValue) => ({ + ...prevValue, + [key]: loaded, + })); + }, + [], + ); + + const allIconsLoaded = useMemo( + () => ( + Object.values(loadedIcons) + .filter(Boolean).length === mapIcons.length + ), + [loadedIcons], + ); + + const hazardPointIconLayer = useMemo>( + () => ({ + type: 'symbol', + paint: { + 'icon-color': COLOR_WHITE, + 'icon-opacity': [ + 'case', + ['boolean', ['feature-state', 'eventVisible'], true], + 1, + 0, + ], + /* + 'icon-opacity-transition': { + duration: 200, + }, + */ + }, + layout: allIconsLoaded ? hazardPointIconLayout : invisibleLayout, + }), + [allIconsLoaded], + ); + + return ( +
+ + + {hazardKeys.map((key) => { + const url = hazardKeyToIconMap[key]; + + if (isNotDefined(url)) { + return null; + } + + return ( + + ); + })} + {activeEventFootprint && ( + + + + + {/* + + */} + + + + + + )} + + + + + + + {boundsSafe && ( + + )} + + + + + + +
+ ); +} + +export default RiskImminentEventMap; diff --git a/app/views/Preparedness/RiskAnalysis/RiskImminentEventMap/mapStyles.ts b/app/views/Preparedness/RiskAnalysis/RiskImminentEventMap/mapStyles.ts new file mode 100644 index 0000000..f39582e --- /dev/null +++ b/app/views/Preparedness/RiskAnalysis/RiskImminentEventMap/mapStyles.ts @@ -0,0 +1,270 @@ +import { + isDefined, + mapToList, +} from '@togglecorp/fujs'; +import type { + CircleLayer, + CirclePaint, + Expression, + FillLayer, + Layout, + LineLayer, + SymbolLayout, +} from 'mapbox-gl'; + +import { type components } from '#generated/riskTypes'; +import cycloneIcon from '#resources/image/risk/cyclone.png'; +import droughtIcon from '#resources/image/risk/drought.png'; +import earthquakeIcon from '#resources/image/risk/earthquake.png'; +import floodIcon from '#resources/image/risk/flood.png'; +import wildfireIcon from '#resources/image/risk/wildfire.png'; +import { + COLOR_BLACK, + COLOR_DARK_GREY, + COLOR_GREEN, + COLOR_ORANGE, + COLOR_RED, + COLOR_WHITE, +} from '#utils/constants'; + +import { hazardTypeToColorMap } from '../../../../utils/risk'; +import { + type RiskLayerSeverity, + type RiskLayerTypes, +} from './utils'; + +type CommonHazardType = components['schemas']['CommonHazardTypeEnumKey']; + +export const hazardKeyToIconMap: Record = { + EQ: earthquakeIcon, + FL: floodIcon, + TC: cycloneIcon, + EP: null, + FI: null, + SS: null, + DR: droughtIcon, + TS: cycloneIcon, + CD: null, + WF: wildfireIcon, +}; + +const mapIcons = mapToList( + hazardKeyToIconMap, + (icon, key) => (icon ? ({ key, icon }) : undefined), +).filter(isDefined); + +const iconImage: SymbolLayout['icon-image'] = [ + 'match', + ['get', 'hazard_type'], + ...(mapIcons).flatMap(({ key }) => [key, key]), + '', +]; + +const severityColorStyle: Expression = [ + 'match', + ['get', 'severity'], + 'red' satisfies RiskLayerSeverity, + COLOR_RED, + 'orange' satisfies RiskLayerSeverity, + COLOR_ORANGE, + 'green' satisfies RiskLayerSeverity, + COLOR_GREEN, + COLOR_DARK_GREY, +]; + +export const geojsonSourceOptions: mapboxgl.GeoJSONSourceRaw = { type: 'geojson' }; +const hazardTypeColorPaint: CirclePaint['circle-color'] = [ + 'match', + ['get', 'hazard_type'], + ...mapToList(hazardTypeToColorMap, (value, key) => [key, value]).flat(), + COLOR_BLACK, +]; + +export const activeHazardPointLayer: Omit = { + type: 'circle', + filter: [ + '==', + ['get', 'type'], + 'hazard-point' satisfies RiskLayerTypes, + ], + paint: { + 'circle-radius': 12, + 'circle-color': severityColorStyle, + 'circle-opacity': 1, + }, +}; + +export const hazardPointLayer: Omit = { + type: 'circle', + paint: { + 'circle-radius': 12, + 'circle-color': hazardTypeColorPaint, + 'circle-opacity': [ + 'case', + ['boolean', ['feature-state', 'eventVisible'], true], + 1, + 0, + ], + /* + 'circle-opacity-transition': { duration: 2000, delay: 0 }, + */ + }, +}; + +export const invisibleLayout: Layout = { + visibility: 'none', +}; + +export const invisibleFillLayer: Omit = { + type: 'fill', + layout: invisibleLayout, +}; + +export const invisibleLineLayer: Omit = { + type: 'line', + layout: invisibleLayout, +}; + +export const invisibleCircleLayer: Omit = { + type: 'circle', + layout: invisibleLayout, +}; + +export const hazardPointIconLayout: SymbolLayout = { + visibility: 'visible', + 'icon-image': iconImage, + 'icon-size': 0.7, + 'icon-allow-overlap': true, +}; + +export const exposureFillLayer: Omit = { + type: 'fill', + filter: [ + '==', + ['get', 'type'], + 'exposure' satisfies RiskLayerTypes, + ], + paint: { + 'fill-color': severityColorStyle, + 'fill-opacity': 0.4, + }, + layout: { visibility: 'visible' }, +}; + +export const exposureFillOutlineLayer: Omit = { + type: 'line', + filter: [ + '==', + ['get', 'type'], + 'exposure' satisfies RiskLayerTypes, + ], + paint: { + 'line-color': COLOR_WHITE, + 'line-width': 1, + 'line-opacity': 1, + }, + layout: { visibility: 'visible' }, +}; + +export const uncertaintyConeLayer: Omit = { + type: 'line', + filter: [ + '==', + ['get', 'type'], + 'uncertainty-cone' satisfies RiskLayerTypes, + ], + paint: { + 'line-color': COLOR_BLACK, + 'line-opacity': 1, + 'line-width': 1, + 'line-dasharray': [5, 7], + }, + layout: { visibility: 'visible' }, +}; + +export const trackLineLayer: Omit = { + type: 'line', + filter: [ + '==', + ['get', 'type'], + 'track-linestring' satisfies RiskLayerTypes, + ], + paint: { + 'line-color': COLOR_BLACK, + 'line-width': 2, + 'line-opacity': 1, + }, + layout: { visibility: 'visible' }, +}; + +/* +export const trackArrowLayer: Omit = { + type: 'symbol', + filter: [ + '==', + ['get', 'type'], + 'track-linestring' satisfies RiskLayerTypes, + ], + paint: { + 'icon-color': COLOR_BLACK, + 'icon-opacity': 0.6, + }, + layout: { + visibility: 'visible', + 'icon-allow-overlap': true, + 'symbol-placement': 'line', + 'icon-image': 'triangle-11', + 'icon-size': 0.6, + 'icon-rotate': 90, + }, +}; +*/ + +export const trackPointLayer: Omit = { + type: 'circle', + filter: [ + '==', + ['get', 'type'], + 'track-point' satisfies RiskLayerTypes, + ], + paint: { + 'circle-radius': 4, + 'circle-color': COLOR_BLACK, + 'circle-opacity': 1, + 'circle-stroke-color': COLOR_WHITE, + 'circle-stroke-width': [ + 'case', + ['boolean', ['get', 'isFuture'], true], + 0, + 1, + ], + }, + layout: { visibility: 'visible' }, +}; + +export const trackPointOuterCircleLayer: Omit = { + type: 'circle', + filter: [ + '==', + ['get', 'type'], + 'track-point' satisfies RiskLayerTypes, + ], + paint: { + 'circle-radius': 12, + 'circle-color': COLOR_BLACK, + 'circle-opacity': [ + 'case', + ['boolean', ['get', 'isFuture'], true], + 0.2, + 0.0, + ], + 'circle-stroke-color': COLOR_WHITE, + 'circle-stroke-width': [ + 'case', + ['boolean', ['get', 'isFuture'], true], + 1, + 0, + ], + }, + layout: { visibility: 'visible' }, +}; diff --git a/app/views/Preparedness/RiskAnalysis/RiskImminentEventMap/styles.module.css b/app/views/Preparedness/RiskAnalysis/RiskImminentEventMap/styles.module.css new file mode 100644 index 0000000..33b6b02 --- /dev/null +++ b/app/views/Preparedness/RiskAnalysis/RiskImminentEventMap/styles.module.css @@ -0,0 +1,27 @@ +.risk-imminent-event-map { + display: flex; + gap: var(--go-ui-spacing-md); + + .side-panel { + flex-basis: calc(14vw + 16rem); + height: var(--go-ui-height-map-md); + } + + .map-container { + flex-grow: 1; + } + + @media screen and (width <= 50rem) { + flex-direction: column; + height: initial; + + .side-panel { + flex-basis: unset; + margin: unset; + border-radius: unset; + box-shadow: unset; + max-height: 70vh; + overflow: auto; + } + } +} diff --git a/app/views/Preparedness/RiskAnalysis/RiskImminentEventMap/utils.ts b/app/views/Preparedness/RiskAnalysis/RiskImminentEventMap/utils.ts new file mode 100644 index 0000000..26afa72 --- /dev/null +++ b/app/views/Preparedness/RiskAnalysis/RiskImminentEventMap/utils.ts @@ -0,0 +1,54 @@ +export type RiskLayerTypes = 'hazard-point' +| 'track-point' +| 'track-point-boundary' +| 'track-linestring' +| 'uncertainty-cone' +| 'exposure' +| 'unknown'; + +export type RiskLayerSeverity = 'red' | 'orange' | 'green' | 'unknown'; + +interface BaseLayerProperties { + type: RiskLayerTypes; +} + +interface HazardPointLayerProperties extends BaseLayerProperties { + type: 'hazard-point'; + severity: RiskLayerSeverity; +} + +interface TrackPointLayerProperties extends BaseLayerProperties { + type: 'track-point'; + // FIXME: added this + isFuture: boolean; +} + +interface TrackPointBoundaryLayerProperties extends BaseLayerProperties { + type: 'track-point-boundary'; +} + +interface TrackLinestringLayerProperties extends BaseLayerProperties { + type: 'track-linestring'; +} + +interface UncertaintyConeLayerProperties extends BaseLayerProperties { + type: 'uncertainty-cone'; + forecastDays: number | undefined; +} + +interface ExposureLayerProperties extends BaseLayerProperties { + type: 'exposure'; + severity: RiskLayerSeverity; +} + +interface UnknownRiskLayerProperties extends BaseLayerProperties { + type: 'unknown'; +} + +export type RiskLayerProperties = HazardPointLayerProperties +| TrackPointLayerProperties +| TrackPointBoundaryLayerProperties +| TrackLinestringLayerProperties +| UncertaintyConeLayerProperties +| ExposureLayerProperties +| UnknownRiskLayerProperties; diff --git a/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/Gdacs/EventDetails/index.tsx b/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/Gdacs/EventDetails/index.tsx new file mode 100644 index 0000000..8255a72 --- /dev/null +++ b/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/Gdacs/EventDetails/index.tsx @@ -0,0 +1,184 @@ +import { + Container, + InlineLayout, + ListView, + TextOutput, +} from '@ifrc-go/ui'; +import { isDefined } from '@togglecorp/fujs'; + +import Link from '#components/Link'; +import { type RiskApiResponse } from '#utils/restRequest'; + +import { type RiskEventDetailProps } from '../../../RiskImminentEventMap'; + +type GdacsResponse = RiskApiResponse<'/api/v1/gdacs/'>; +type GdacsItem = NonNullable[number]; +type GdacsExposure = RiskApiResponse<'/api/v1/gdacs/{id}/exposure/'>; + +interface GdacsEventDetails { + Class?: string; + affectedcountries?: { + iso3: string; + countryname: string; + }[]; + alertlevel?: string; + alertscore?: number; + country?: string; + countryonland?: string; + description?: string; + episodealertlevel?: string; + episodealertscore?: number; + episodeid?: number; + eventid?: number; + eventname?: string; + eventtype?: string; + fromdate?: string; + glide?: string; + htmldescription?: string; + icon?: string; + iconoverall?: null, + iscurrent?: string; + iso3?: string; + istemporary?: string; + name?: string; + polygonlabel?: string; + todate?: string; + severitydata?: { + severity?: number; + severitytext?: string; + severityunit?: string; + }, + source?: string; + sourceid?: string; + url?: { + report?: string; + details?: string; + geometry?: string; + }, +} + +interface GdacsPopulationExposure { + death?: number; + displaced?: number; + exposed_population?: string; + people_affected?: string; + impact?: string; +} + +type Props = RiskEventDetailProps; + +function EventDetails(props: Props) { + const { + data: { + event_details, + }, + exposure, + pending, + children, + } = props; + + const populationExposure = exposure?.population_exposure as GdacsPopulationExposure | undefined; + const eventDetails = event_details as GdacsEventDetails | undefined; + + return ( + + + {isDefined(eventDetails?.source) && ( + + )} + {isDefined(populationExposure?.death) && ( + + )} + {isDefined(populationExposure?.displaced) && ( + + )} + {isDefined(populationExposure?.exposed_population) && ( + + )} + {isDefined(populationExposure?.people_affected) && ( + + )} + {isDefined(populationExposure?.impact) && ( + + )} + {isDefined(eventDetails?.severitydata) + && (isDefined(eventDetails) && (eventDetails?.eventtype) && !(eventDetails.eventtype === 'FL')) && ( + + )} + {isDefined(eventDetails?.alertlevel) && ( + + )} + {isDefined(eventDetails) + && isDefined(eventDetails.url) + && isDefined(eventDetails.url.report) + && ( + + More Details + + )} + /> + )} + {/* NOTE: Intentional additional div to maintain gap */} + {children &&
} + {children} + + + ); +} + +export default EventDetails; diff --git a/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/Gdacs/EventListItem/index.tsx b/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/Gdacs/EventListItem/index.tsx new file mode 100644 index 0000000..de197a7 --- /dev/null +++ b/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/Gdacs/EventListItem/index.tsx @@ -0,0 +1,47 @@ +import { TextOutput } from '@ifrc-go/ui'; + +import ImminentEventListItem from '#components/ImminentEventListItem'; +import { type RiskApiResponse } from '#utils/restRequest'; + +import { type RiskEventListItemProps } from '../../../RiskImminentEventMap'; + +type ImminentEventResponse = RiskApiResponse<'/api/v1/gdacs/'>; +type GdacsItem = NonNullable[number]; + +type Props = RiskEventListItemProps; + +function EventListItem(props: Props) { + const { + data: { + id, + hazard_name, + start_date, + }, + expanded, + onExpandClick, + className, + children, + } = props; + + return ( + + )} + > + {children} + + ); +} + +export default EventListItem; diff --git a/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/Gdacs/index.tsx b/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/Gdacs/index.tsx new file mode 100644 index 0000000..76b7d1a --- /dev/null +++ b/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/Gdacs/index.tsx @@ -0,0 +1,291 @@ +import { useCallback } from 'react'; +import { numericIdSelector } from '@ifrc-go/ui/utils'; +import { + isDefined, + isNotDefined, + isObject, +} from '@togglecorp/fujs'; +import { type LngLatBoundsLike } from 'mapbox-gl'; + +import { + type RiskApiResponse, + useRiskLazyRequest, + useRiskRequest, +} from '#utils/restRequest'; + +import { isValidFeatureCollection } from '../../../../../utils/risk'; +import RiskImminentEventMap, { type EventPointFeature } from '../../RiskImminentEventMap'; +import type { + RiskLayerProperties, + RiskLayerSeverity, +} from '../../RiskImminentEventMap/utils'; +import EventDetails from './EventDetails'; +import EventListItem from './EventListItem'; + +type ImminentEventResponse = RiskApiResponse<'/api/v1/gdacs/'>; +type EventItem = NonNullable[number]; + +function hazardTypeSelector(item: EventItem) { + return item.hazard_type; +} + +interface CommonFeatureProperties { + Class: string; +} + +type FeatureAlertLevel = 'Green' | 'Red' | 'Orange'; + +interface HazardPointFeatureProperties extends CommonFeatureProperties { + alertlevel: FeatureAlertLevel, +} + +const severityMapping: Record = { + Red: 'red', + Orange: 'orange', + Green: 'green', +}; + +// Currently observed classes for TC are +// Point_ are points +// Poly_ are polygons +// Line_ are linestrings +// Point_0 is track point +// Poly_Green is exposure polygon +// Poly_Polygon_Point_0 is circle around the Point_0 +// Line_Line_0 is a line from Point_0 to Point_1 +// Poly_Cones is cone of uncertainty +function getLayerProperties( + feature: GeoJSON.Feature, + hazardDate: string | undefined, +): RiskLayerProperties { + if (isNotDefined(feature.properties) || !('Class' in feature.properties)) { + return { + type: 'unknown', + }; + } + + const { + Class: featureClass, + } = feature.properties; + + const splits = featureClass.split('_'); + + if (splits[0] === 'Point') { + if (splits[1] === 'Centroid') { + const severityStr = (feature.properties as HazardPointFeatureProperties).alertlevel; + + return { + type: 'hazard-point', + severity: severityMapping[severityStr] ?? 'unknown', + }; + } + + // Converting format from 'dd/MM/yyyy hh:mm:ss' to 'yyyy-MM-ddThh:mm:ss.sssZ' + const [date, time] = feature.properties.trackdate.split(' '); + const [d, m, y] = date.split('/'); + const standardDateTime = `${y}-${m}-${d}T${time}.000Z`; + + return { + type: 'track-point', + isFuture: hazardDate + ? new Date(standardDateTime).getTime() > new Date(hazardDate).getTime() + : false, + }; + } + + if (splits[0] === 'Line') { + return { + type: 'track-linestring', + }; + } + + if (splits[0] === 'Poly') { + if (splits[1] === 'Cones') { + return { + type: 'uncertainty-cone', + forecastDays: undefined, + }; + } + + if (splits[1] === 'Red' || splits[1] === 'Orange' || splits[1] === 'Green') { + return { + type: 'exposure', + severity: severityMapping[splits[1]] ?? 'unknown', + }; + } + + if (splits[1] === 'Polygon' && splits[2] === 'Point') { + return { + type: 'track-point-boundary', + }; + } + } + + return { + type: 'unknown', + }; +} + +type BaseProps = { + title: React.ReactNode; + bbox: LngLatBoundsLike | undefined; +} + +type Props = BaseProps & ({ + variant: 'global'; +} | { + variant: 'region'; + regionId: number; +} | { + variant: 'country'; + iso3: string; +}) + +function Gdacs(props: Props) { + const { + title, + bbox, + variant, + } = props; + + const { + pending: pendingCountryRiskResponse, + response: countryRiskResponse, + } = useRiskRequest({ + apiType: 'risk', + // eslint-disable-next-line react/destructuring-assignment + skip: (variant === 'region' && isNotDefined(props.regionId)) + // eslint-disable-next-line react/destructuring-assignment + || (variant === 'country' && isNotDefined(props.iso3)), + url: '/api/v1/gdacs/', + query: { + limit: 9999, + // eslint-disable-next-line react/destructuring-assignment + iso3: variant === 'country' ? props.iso3 : undefined, + // eslint-disable-next-line react/destructuring-assignment + region: variant === 'region' ? [props.regionId] : undefined, + }, + }); + + const { + response: exposureResponse, + pending: exposureResponsePending, + trigger: fetchExposure, + } = useRiskLazyRequest<'/api/v1/gdacs/{id}/exposure/', { + eventId: number | string, + }>({ + apiType: 'risk', + url: '/api/v1/gdacs/{id}/exposure/', + pathVariables: ({ eventId }) => ({ id: Number(eventId) }), + }); + + const pointFeatureSelector = useCallback( + (event: EventItem): EventPointFeature | undefined => { + const { + id, + latitude, + longitude, + hazard_type, + } = event; + + if ( + isNotDefined(latitude) + || isNotDefined(longitude) + || isNotDefined(hazard_type) + ) { + return undefined; + } + + return { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [longitude, latitude], + }, + properties: { + id, + hazard_type, + }, + }; + }, + [], + ); + + const footprintSelector = useCallback( + (exposure: RiskApiResponse<'/api/v1/gdacs/{id}/exposure/'> | undefined) => { + if (isNotDefined(exposure)) { + return undefined; + } + + const { footprint_geojson } = exposure; + + if (isNotDefined(footprint_geojson)) { + return undefined; + } + + // FIXME: the type from server is not correct + const footprint = isValidFeatureCollection(footprint_geojson) + ? footprint_geojson + : undefined; + + const hazardDate = (isDefined(footprint) + && 'metadata' in footprint + && isObject(footprint.metadata) + && 'todate' in footprint.metadata + ) + ? String(footprint.metadata.todate) + : undefined; + + const geoJson: GeoJSON.FeatureCollection = { + type: 'FeatureCollection' as const, + features: [ + ...footprint?.features?.map( + (feature) => ({ + ...feature, + properties: { + ...feature.properties, + // NOTE: the todate format is 'dd MMM yyyy hh:mm:ss' + ...getLayerProperties(feature, hazardDate), + }, + }), + ) ?? [], + ].filter(isDefined), + }; + + return geoJson; + }, + [], + ); + + const handleActiveEventChange = useCallback( + (eventId: number | undefined) => { + if (isDefined(eventId)) { + fetchExposure({ eventId }); + } else { + fetchExposure(undefined); + } + }, + [fetchExposure], + ); + + return ( + + ); +} + +export default Gdacs; diff --git a/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/MeteoSwiss/EventDetails/index.tsx b/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/MeteoSwiss/EventDetails/index.tsx new file mode 100644 index 0000000..6a34439 --- /dev/null +++ b/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/MeteoSwiss/EventDetails/index.tsx @@ -0,0 +1,247 @@ +import { + useCallback, + useMemo, +} from 'react'; +import { + Container, + DateOutput, + Description, + ListView, + NumberOutput, + TextOutput, +} from '@ifrc-go/ui'; +import { + resolveToComponent, + resolveToString, +} from '@ifrc-go/ui/utils'; +import { + compareString, + isDefined, + isNotDefined, +} from '@togglecorp/fujs'; + +import Link from '#components/Link'; +import { type RiskApiResponse } from '#utils/restRequest'; + +type MeteoSwissResponse = RiskApiResponse<'/api/v1/meteoswiss/'>; +type MeteoSwissItem = NonNullable[number]; +// type MeteoSwissExposure = RiskApiResponse<'/api/v1/meteoswiss/{id}/exposure/'>; + +const UPDATED_AT_FORMAT = 'yyyy-MM-dd, hh:mm'; + +interface Props { + data: MeteoSwissItem; + // exposure: MeteoSwissExposure | undefined; + pending: boolean; +} +const SAFFIR_SIMPSON_SCALE = [ + { threshold: 33, description: 'equivalent to tropical storm or above' }, + { threshold: 43, description: 'equivalent to a category 1 hurricane or above' }, + { threshold: 50, description: 'equivalent to a category 2 hurricane or above' }, + { threshold: 59, description: 'equivalent to a category 3 hurricane or above' }, + { threshold: 71, description: 'equivalent to a category 4 hurricane or above' }, +]; + +const CATEGORY_5_DESCRIPTION = 'equivalent to a category 5 hurricane or above'; +function EventDetails(props: Props) { + const { + data: { + country_details, + start_date, + updated_at, + hazard_name, + model_name, + event_details, + }, + pending, + } = props; + + const getSaffirSimpsonScaleDescription = useCallback((windspeed: number) => ( + SAFFIR_SIMPSON_SCALE.find(({ threshold }) => windspeed < threshold)?.description + ?? CATEGORY_5_DESCRIPTION + ), []); + + const impactList = useMemo( + () => ( + // FIXME: typings should be fixed in the server + (event_details as unknown as unknown[])?.map((event: unknown, i: number) => { + if ( + typeof event !== 'object' + || isNotDefined(event) + || !('mean' in event) + || !('impact_type' in event) + || !('five_perc' in event) + || !('ninety_five_perc' in event) + ) { + return undefined; + } + + const { + impact_type, + five_perc, + ninety_five_perc, + mean, + } = event; + + const valueSafe = typeof mean === 'number' ? Math.round(mean) : undefined; + const fivePercentValue = typeof five_perc === 'number' ? Math.round(five_perc) : undefined; + const ninetyFivePercentValue = typeof ninety_five_perc === 'number' ? Math.round(ninety_five_perc) : undefined; + if (isNotDefined(valueSafe) || valueSafe === 0) { + return undefined; + } + + if (typeof impact_type !== 'string') { + return undefined; + } + + if (impact_type === 'direct_economic_damage') { + return { + key: i, + type: 'economic', + value: valueSafe, + fivePercentValue, + ninetyFivePercentValue, + label: 'Wind related direct economic damage', + unit: 'USD', + }; + } + + if (impact_type.startsWith('exposed_population_')) { + const windspeed = Number.parseInt( + impact_type.split('exposed_population_')[1]!, + 10, + ); + + if (isNotDefined(windspeed)) { + return undefined; + } + + return { + key: i, + type: 'exposure', + value: valueSafe, + fivePercentValue, + ninetyFivePercentValue, + label: resolveToString( + 'Estimated people exposed to windspeed above {windspeed}m/s. ({saffirSimpsonScale})', + { + windspeed, + saffirSimpsonScale: getSaffirSimpsonScaleDescription(windspeed), + }, + ), + unit: 'People', + }; + } + + return undefined; + }).filter(isDefined).sort((a, b) => compareString(b.type, a.type)) + ), + [ + event_details, + getSaffirSimpsonScaleDescription, + ], + ); + + // TODO: add exposure details + return ( + + + + {impactList.map((impact) => ( + beta, + }, + )} + value={resolveToComponent( + '{value} ({fivePercent} - {ninetyFivePercent}) {unit}', + { + value: ( + + ), + fivePercent: ( + + ), + ninetyFivePercent: ( + + ), + unit: impact.unit, + }, + )} + strongValue + /> + ))} + + These impact estimates are derived from + a model and come with very high uncertainty. + More information will be added soon. + + + +
+ {resolveToComponent( + '{model} has forecasted a tropical cyclone on {updatedAt}. The tropical cyclone named {eventName} is forecasted to impact {countryName} from {eventDate}.', + { + model: model_name ?? '--', + updatedAt: ( + + ), + eventName: hazard_name, + countryName: country_details?.name ?? '--', + eventDate: , + }, + )} +
+
+ {resolveToComponent( + 'Please also consider {link} and classification of {classificationLink}.', + { + link: ( + + authoritative information about the hazard + + ), + classificationLink: ( + + tropical storm + + ), + }, + )} +
+
+ ); +} + +export default EventDetails; diff --git a/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/MeteoSwiss/EventListItem/index.tsx b/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/MeteoSwiss/EventListItem/index.tsx new file mode 100644 index 0000000..7cc8668 --- /dev/null +++ b/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/MeteoSwiss/EventListItem/index.tsx @@ -0,0 +1,73 @@ +import { + useEffect, + useRef, +} from 'react'; +import { TextOutput } from '@ifrc-go/ui'; + +import ImminentEventListItem from '#components/ImminentEventListItem'; +import { type RiskApiResponse } from '#utils/restRequest'; + +import { type RiskEventListItemProps } from '../../../RiskImminentEventMap'; + +type ImminentEventResponse = RiskApiResponse<'/api/v1/meteoswiss/'>; +type EventItem = NonNullable[number]; + +type Props = RiskEventListItemProps; + +function EventListItem(props: Props) { + const { + data: { + id, + hazard_type_display, + country_details, + start_date, + hazard_name, + }, + expanded, + onExpandClick, + className, + children, + } = props; + + const hazardName = `${hazard_type_display} - ${country_details?.name ?? hazard_name}`; + + const elementRef = useRef(null); + + useEffect( + () => { + if (expanded && elementRef.current) { + const y = window.scrollY; + const x = window.scrollX; + elementRef.current.scrollIntoView({ + behavior: 'instant', + block: 'start', + }); + // NOTE: We need to scroll back because scrollIntoView also + // scrolls the parent container + window.scroll(x, y); + } + }, + [expanded], + ); + + return ( + + )} + > + {children} + + ); +} + +export default EventListItem; diff --git a/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/MeteoSwiss/index.tsx b/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/MeteoSwiss/index.tsx new file mode 100644 index 0000000..7a68d15 --- /dev/null +++ b/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/MeteoSwiss/index.tsx @@ -0,0 +1,232 @@ +import { useCallback } from 'react'; +import { numericIdSelector } from '@ifrc-go/ui/utils'; +import { + isDefined, + isNotDefined, + isObject, +} from '@togglecorp/fujs'; +import type { LngLatBoundsLike } from 'mapbox-gl'; + +import { + type RiskApiResponse, + useRiskLazyRequest, + useRiskRequest, +} from '#utils/restRequest'; + +import { isValidFeatureCollection } from '../../../../../utils/risk'; +import RiskImminentEventMap, { type EventPointFeature } from '../../RiskImminentEventMap'; +import { type RiskLayerProperties } from '../../RiskImminentEventMap/utils'; +import EventDetails from './EventDetails'; +import EventListItem from './EventListItem'; + +type ImminentEventResponse = RiskApiResponse<'/api/v1/meteoswiss/'>; +type EventItem = NonNullable[number]; + +function hazardTypeSelector(item: EventItem) { + return item.hazard_type; +} + +function getLayerProperties( + feature: GeoJSON.Feature, +): RiskLayerProperties { + if (isNotDefined(feature) + || isNotDefined(feature.properties) + || isNotDefined(feature.geometry) + ) { + return { + type: 'unknown', + }; + } + + const geometryType = feature.geometry.type; + + if (geometryType === 'Point' || geometryType === 'MultiPoint') { + // FIXME: calculate isFuture + return { type: 'track-point', isFuture: true }; + } + + if (geometryType === 'LineString' || geometryType === 'MultiLineString') { + return { type: 'track-linestring' }; + } + + if (geometryType === 'Polygon' || geometryType === 'MultiPolygon') { + return { + type: 'exposure', + severity: 'unknown', + }; + } + + return { + type: 'unknown', + }; +} + +type BaseProps = { + title: React.ReactNode; + bbox: LngLatBoundsLike | undefined; +} + +type Props = BaseProps & ({ + variant: 'global'; +} | { + variant: 'region'; + regionId: number; +} | { + variant: 'country'; + iso3: string; +}) + +function MeteoSwiss(props: Props) { + const { + title, + bbox, + variant, + } = props; + + const { + pending: pendingCountryRiskResponse, + response: countryRiskResponse, + } = useRiskRequest({ + apiType: 'risk', + // eslint-disable-next-line react/destructuring-assignment + skip: (variant === 'region' && isNotDefined(props.regionId)) + // eslint-disable-next-line react/destructuring-assignment + || (variant === 'country' && isNotDefined(props.iso3)), + url: '/api/v1/meteoswiss/', + query: { + limit: 9999, + // eslint-disable-next-line react/destructuring-assignment + iso3: variant === 'country' ? props.iso3 : undefined, + // eslint-disable-next-line react/destructuring-assignment + region: variant === 'region' ? [props.regionId] : undefined, + }, + }); + + const { + response: exposureResponse, + pending: exposureResponsePending, + trigger: fetchExposure, + } = useRiskLazyRequest<'/api/v1/meteoswiss/{id}/exposure/', { + eventId: number | string, + }>({ + apiType: 'risk', + url: '/api/v1/meteoswiss/{id}/exposure/', + pathVariables: ({ eventId }) => ({ id: Number(eventId) }), + }); + + const pointFeatureSelector = useCallback( + (event: EventItem): EventPointFeature | undefined => { + const { + id, + latitude, + longitude, + hazard_type, + } = event; + + if ( + isNotDefined(latitude) + || isNotDefined(longitude) + || isNotDefined(hazard_type) + ) { + return undefined; + } + + return { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [longitude, latitude], + }, + properties: { + id, + hazard_type, + }, + }; + }, + [], + ); + + const footprintSelector = useCallback( + (exposure: RiskApiResponse<'/api/v1/meteoswiss/{id}/exposure/'> | undefined) => { + if (isNotDefined(exposure)) { + return undefined; + } + + // FIXME: fix typing in server (low priority) + const footprint_geojson = 'footprint_geojson' in exposure + && isObject(exposure.footprint_geojson) + && 'footprint_geojson' in exposure.footprint_geojson + ? exposure.footprint_geojson?.footprint_geojson + : undefined; + + if (isNotDefined(footprint_geojson)) { + return undefined; + } + + const footprint = isValidFeatureCollection(footprint_geojson) + ? footprint_geojson + : undefined; + + const geoJson: GeoJSON.FeatureCollection = { + type: 'FeatureCollection' as const, + features: [ + ...footprint?.features?.map( + (feature) => { + if (isNotDefined(feature)) { + return undefined; + } + + const { geometry } = feature; + if (isNotDefined(geometry)) { + return undefined; + } + + return { + ...feature, + properties: { + ...feature.properties, + ...getLayerProperties(feature), + }, + }; + }, + ) ?? [], + ].filter(isDefined), + }; + + return geoJson; + }, + [], + ); + + const handleActiveEventChange = useCallback( + (eventId: number | undefined) => { + if (isDefined(eventId)) { + fetchExposure({ eventId }); + } else { + fetchExposure(undefined); + } + }, + [fetchExposure], + ); + + return ( + + ); +} + +export default MeteoSwiss; diff --git a/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/Pdc/EventDetails/index.tsx b/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/Pdc/EventDetails/index.tsx new file mode 100644 index 0000000..8bd55be --- /dev/null +++ b/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/Pdc/EventDetails/index.tsx @@ -0,0 +1,115 @@ +import { + Container, + ListView, + TextOutput, +} from '@ifrc-go/ui'; + +import { type RiskApiResponse } from '#utils/restRequest'; + +import { type RiskEventDetailProps } from '../../../RiskImminentEventMap'; + +type PdcResponse = RiskApiResponse<'/api/v1/pdc/'>; +type PdcEventItem = NonNullable[number]; +type PdcExposure = RiskApiResponse<'/api/v1/pdc/{id}/exposure/'>; + +type Props = RiskEventDetailProps; + +function EventDetails(props: Props) { + const { + data: { + pdc_created_at, + pdc_updated_at, + description, + }, + exposure, + pending, + children, + } = props; + + interface Exposure { + value?: number | null; + valueFormatted?: string | null; + } + + // NOTE: these are stored as json so we don't have typings for these + const popExposure = exposure?.population_exposure as { + total?: Exposure | null; + households?: Exposure | null; + vulnerable?: Exposure | null; + } | null; + + // NOTE: these are stored as json so we don't have typings for these + const capitalExposure = exposure?.capital_exposure as { + total?: Exposure | null; + school?: Exposure | null; + hospital?: Exposure | null; + } | null; + + return ( + + + + + + + + + + + + {children} + + + ); +} + +export default EventDetails; diff --git a/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/Pdc/EventListItem/index.tsx b/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/Pdc/EventListItem/index.tsx new file mode 100644 index 0000000..fe3f733 --- /dev/null +++ b/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/Pdc/EventListItem/index.tsx @@ -0,0 +1,46 @@ +import { TextOutput } from '@ifrc-go/ui'; + +import ImminentEventListItem from '#components/ImminentEventListItem'; +import { type RiskApiResponse } from '#utils/restRequest'; + +import { type RiskEventListItemProps } from '../../../RiskImminentEventMap'; + +type ImminentEventResponse = RiskApiResponse<'/api/v1/pdc/'>; +type EventItem = NonNullable[number]; + +type Props = RiskEventListItemProps; + +function EventListItem(props: Props) { + const { + data: { + id, + hazard_name, + start_date, + }, + expanded, + onExpandClick, + className, + children, + } = props; + + return ( + + )} + expanded={expanded} + eventId={id} + onExpandClick={onExpandClick} + > + {children} + + ); +} + +export default EventListItem; diff --git a/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/Pdc/index.tsx b/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/Pdc/index.tsx new file mode 100644 index 0000000..8c41810 --- /dev/null +++ b/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/Pdc/index.tsx @@ -0,0 +1,266 @@ +import { + useCallback, + useState, +} from 'react'; +import { numericIdSelector } from '@ifrc-go/ui/utils'; +import { + isDefined, + isNotDefined, +} from '@togglecorp/fujs'; +import { type LngLatBoundsLike } from 'mapbox-gl'; + +import { + type RiskApiResponse, + useRiskLazyRequest, + useRiskRequest, +} from '#utils/restRequest'; + +import { + isValidFeature, + isValidPointFeature, +} from '../../../../../utils/risk'; +import RiskImminentEventMap, { type EventPointFeature } from '../../RiskImminentEventMap'; +import { type RiskLayerProperties } from '../../RiskImminentEventMap/utils'; +import EventDetails from './EventDetails'; +import EventListItem from './EventListItem'; + +type ImminentEventResponse = RiskApiResponse<'/api/v1/pdc/'>; +type EventItem = NonNullable[number]; + +function hazardTypeSelector(item: EventItem) { + return item.hazard_type; +} + +type BaseProps = { + title: React.ReactNode; + bbox: LngLatBoundsLike | undefined; +} + +type Props = BaseProps & ({ + variant: 'global'; +} | { + variant: 'region'; + regionId: number; +} | { + variant: 'country'; + iso3: string; +}) + +function Pdc(props: Props) { + const { + title, + bbox, + variant, + } = props; + + const [activeEventId, setActiveEventId] = useState(); + + const { + pending: pendingCountryRiskResponse, + response: countryRiskResponse, + } = useRiskRequest({ + apiType: 'risk', + // eslint-disable-next-line react/destructuring-assignment + skip: (variant === 'region' && isNotDefined(props.regionId)) + // eslint-disable-next-line react/destructuring-assignment + || (variant === 'country' && isNotDefined(props.iso3)), + url: '/api/v1/pdc/', + query: { + limit: 9999, + // eslint-disable-next-line react/destructuring-assignment + iso3: variant === 'country' ? props.iso3 : undefined, + // eslint-disable-next-line react/destructuring-assignment + region: variant === 'region' ? [props.regionId] : undefined, + }, + }); + + const { + response: exposureResponse, + pending: exposureResponsePending, + trigger: fetchExposure, + } = useRiskLazyRequest<'/api/v1/pdc/{id}/exposure/', { + eventId: number | string, + }>({ + apiType: 'risk', + url: '/api/v1/pdc/{id}/exposure/', + pathVariables: ({ eventId }) => ({ id: Number(eventId) }), + }); + + const pointFeatureSelector = useCallback( + (event: EventItem): EventPointFeature | undefined => { + const { + id, + latitude, + longitude, + hazard_type, + } = event; + + if ( + isNotDefined(latitude) + || isNotDefined(longitude) + || isNotDefined(hazard_type) + ) { + return undefined; + } + + return { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [longitude, latitude], + }, + properties: { + id, + hazard_type, + }, + }; + }, + [], + ); + + const activeEvent = countryRiskResponse?.results?.find( + (item) => item.id === activeEventId, + ); + + const footprintSelector = useCallback( + (exposure: RiskApiResponse<'/api/v1/pdc/{id}/exposure/'> | undefined) => { + if (isNotDefined(exposure)) { + return undefined; + } + + const { + footprint_geojson, + storm_position_geojson, + cyclone_five_days_cou, + cyclone_three_days_cou, + } = exposure; + + // FIXME: showing five days cou when three days cou is not available + const cyclone_cou = (cyclone_three_days_cou as unknown as object[] | null)?.[0] + ?? (cyclone_five_days_cou as unknown as object[] | null)?.[0]; + + if (isNotDefined(footprint_geojson) && isNotDefined(storm_position_geojson)) { + return undefined; + } + + const footprint = isValidFeature(footprint_geojson) ? footprint_geojson : undefined; + // FIXME: fix typing in server (low priority) + const stormPositions = (storm_position_geojson as unknown as unknown[] | undefined) + ?.filter(isValidPointFeature); + + // FIXME: fix typing in server (low priority) + const forecastUncertainty = isValidFeature(cyclone_cou) + ? cyclone_cou + : undefined; + + // severity + // WARNING: Adverse or significant impacts to population are imminent or occurring. + // WATCH: Conditions are possible for adverse or significant impacts to population. + // ADVISORY: Conditions are possible for limited or minor impacts to population + // INFORMATION: Conditions are possible for limited or minor impacts to population + + // advisory_date: "28-Sep-2024" + // advisory_number: 4 + // advisory_time: "0000Z" + // hazard_name: "Super Typhoon - Krathon" + // + // forecast_date_time : "2023 SEP 04, 00:00Z" + // severity : "WARNING" + // storm_name : "HAIKUI" + // track_heading : "WNW" + // wind_speed_mph : 75 + // track_speed_mph: xx + + const geoJson: GeoJSON.FeatureCollection = { + type: 'FeatureCollection' as const, + features: [ + footprint ? { + ...footprint, + properties: { + ...footprint.properties, + type: 'exposure' as const, + severity: 'unknown' as const, + }, + } : undefined, + forecastUncertainty ? { + ...forecastUncertainty, + properties: { + ...forecastUncertainty.properties, + type: 'uncertainty-cone' as const, + forecastDays: 3, + }, + } : undefined, + stormPositions ? { + type: 'Feature' as const, + geometry: { + type: 'LineString' as const, + coordinates: stormPositions.map( + (pointFeature) => ( + pointFeature.geometry.coordinates + ), + ), + }, + properties: { + type: 'track-linestring' as const, + }, + } : undefined, + ...stormPositions?.map( + (pointFeature) => ({ + ...pointFeature, + properties: { + ...pointFeature.properties, + type: 'track-point' as const, + isFuture: ( + activeEvent + && activeEvent.pdc_updated_at + && pointFeature.properties?.forecast_date_time + ? ( + new Date(pointFeature.properties.forecast_date_time) + > new Date(activeEvent.pdc_updated_at) + ) + : false + ), + }, + }), + ) ?? [], + ].filter(isDefined), + }; + + return geoJson; + }, + [activeEvent], + ); + + const handleActiveEventChange = useCallback( + (eventId: number | undefined) => { + if (isDefined(eventId)) { + fetchExposure({ eventId }); + } else { + fetchExposure(undefined); + } + setActiveEventId(eventId); + }, + [fetchExposure], + ); + + return ( + + ); +} + +export default Pdc; diff --git a/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/WfpAdam/EventDetails/index.tsx b/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/WfpAdam/EventDetails/index.tsx new file mode 100644 index 0000000..bf90b75 --- /dev/null +++ b/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/WfpAdam/EventDetails/index.tsx @@ -0,0 +1,385 @@ +import { useMemo } from 'react'; +import { + Container, + ListView, + TextOutput, + Tooltip, +} from '@ifrc-go/ui'; +import { + getPercentage, + maxSafe, + resolveToString, + roundSafe, +} from '@ifrc-go/ui/utils'; +import { + compareDate, + isDefined, + isFalsyString, + isNotDefined, + unique, +} from '@togglecorp/fujs'; + +import Link from '#components/Link'; +import { type RiskApiResponse } from '#utils/restRequest'; +import { + isValidFeatureCollection, + isValidPointFeature, +} from '#utils/risk'; + +import { type RiskEventDetailProps } from '../../../RiskImminentEventMap'; + +import styles from './styles.module.css'; + +type WfpAdamResponse = RiskApiResponse<'/api/v1/adam-exposure/'>; +type WfpAdamItem = NonNullable[number]; +type WfpAdamExposure = RiskApiResponse<'/api/v1/adam-exposure/{id}/exposure/'>; + +interface WfpAdamPopulationExposure { + exposure_60_kmh?: number; + exposure_90_kmh?: number; + exposure_120_kmh?: number; +} + +interface WfpAdamEventDetails { + event_id: string; + mag?: number; + mmni?: number; + url?: { + map?: string; + shakemap?: string; + population?: string; + population_csv?: string; + wind?: string; + rainfall?: string; + shapefile?: string + } + iso3?: string; + depth?: number; + place?: string; + title?: string; + latitude?: number; + longitude?: number; + mag_type?: string; + admin1_name?: string; + published_at?: string; + population_impact?: number; + country?: number | null; + alert_sent?: boolean; + alert_level?: 'Red' | 'Orange' | 'Green' | 'Cones' | null; + from_date?: string; + to_date?: string; + wind_speed?: number; + effective_date?: string; + date_processed?: string; + population?: number; + dashboard_url?: string; + flood_area?: number; + fl_croplnd?: number; + source?: string; + sitrep?: string; +} + +type Props = RiskEventDetailProps; + +function EventDetails(props: Props) { + const { + data: { + event_details, + }, + exposure, + pending, + children, + } = props; + + const stormPoints = useMemo( + () => { + if (isNotDefined(exposure)) { + return undefined; + } + + const { storm_position_geojson } = exposure; + + const geoJson = isValidFeatureCollection(storm_position_geojson) + ? storm_position_geojson + : undefined; + + const points = geoJson?.features.map( + (pointFeature) => { + if ( + !isValidPointFeature(pointFeature) + || isNotDefined(pointFeature.properties) + ) { + return undefined; + } + + const { + wind_speed, + track_date, + } = pointFeature.properties; + + if (isNotDefined(wind_speed) || isFalsyString(track_date)) { + return undefined; + } + + const date = new Date(track_date); + + return { + // NOTE: using date.getTime() caused duplicate ids + id: track_date, + windSpeed: wind_speed, + date, + }; + }, + ).filter(isDefined).sort( + (a, b) => compareDate(a.date, b.date), + ) ?? []; + + return unique(points, (point) => point.id); + }, + [exposure], + ); + + const eventDetails = event_details as WfpAdamEventDetails | undefined; + + // eslint-disable-next-line max-len + const populationExposure = exposure?.population_exposure as WfpAdamPopulationExposure | undefined; + + const dashboardUrl = eventDetails?.dashboard_url ?? eventDetails?.url?.map; + const populationImpact = roundSafe(eventDetails?.population_impact) + ?? roundSafe(eventDetails?.population); + const maxWindSpeed = maxSafe( + stormPoints?.map(({ windSpeed }) => windSpeed), + ); + + // TODO: Add exposure details + // TODO: Update stylings + return ( + + + {stormPoints && stormPoints.length > 0 && isDefined(maxWindSpeed) && ( + /* TODO: use proper svg charts */ +
+
+ {stormPoints.map( + (point) => ( +
+ +
+
+ ), + )} +
+
+ Windspeed over time +
+
+ )} + {isDefined(eventDetails) + && (isDefined(eventDetails.url) || isDefined(eventDetails.dashboard_url)) && ( + + + {isDefined(dashboardUrl) && ( + + Dashboard + + )} + {isDefined(eventDetails?.url) && ( + <> + {isDefined(eventDetails.url.shakemap) && ( + + Shakemap + + )} + {isDefined(eventDetails.url.population) && ( + + Population Table + + )} + {isDefined(eventDetails.url.wind) && ( + + Wind + + )} + {isDefined(eventDetails.url.rainfall) && ( + + Rainfall + + )} + {isDefined(eventDetails.url.shapefile) && ( + + Shapefile + + )} + + )} + + + )} +
+ {isDefined(eventDetails?.wind_speed) && ( + + )} + {isDefined(populationImpact) && ( + + )} +
+
+ {isDefined(eventDetails?.source) && ( + + )} + {isDefined(eventDetails?.sitrep) && ( + + )} + {isDefined(eventDetails?.mag) && ( + + )} + {isDefined(eventDetails?.depth) && ( + + )} + {isDefined(eventDetails?.alert_level) && ( + + )} + {isDefined(eventDetails?.effective_date) && ( + + )} + {isDefined(eventDetails?.from_date) && ( + + )} + {isDefined(eventDetails?.to_date) && ( + + )} +
+
+ {isDefined(populationExposure?.exposure_60_kmh) && ( + + )} + {isDefined(populationExposure?.exposure_90_kmh) && ( + + )} + {isDefined(populationExposure?.exposure_120_kmh) && ( + + )} + {isDefined(eventDetails?.flood_area) && ( + + )} + {isDefined(eventDetails?.fl_croplnd) && ( + + )} +
+ {children} + + + ); +} + +export default EventDetails; diff --git a/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/WfpAdam/EventDetails/styles.module.css b/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/WfpAdam/EventDetails/styles.module.css new file mode 100644 index 0000000..20c79cf --- /dev/null +++ b/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/WfpAdam/EventDetails/styles.module.css @@ -0,0 +1,54 @@ +.event-details { + .content { + display: flex; + flex-direction: column; + gap: var(--go-ui-spacing-lg); + + .wind-speed-chart { + display: flex; + flex-direction: column; + gap: var(--go-ui-spacing-xs); + + .bar-list-container { + display: flex; + align-items: stretch; + height: 10rem; + + .bar-container { + display: flex; + align-items: flex-end; + flex-basis: 0; + flex-grow: 1; + justify-content: center; + transition: var(--go-ui-duration-transition-medium) background-color ease-in-out; + border-top-left-radius: 2pt; + border-top-right-radius: 2pt; + background-color: transparent; + + .bar { + background-color: var(--go-ui-color-primary-red); + width: 4pt; + height: 100%; + } + + &:hover { + background-color: var(--go-ui-color-background-hover); + } + } + } + + .chart-label { + text-align: center; + color: var(--go-ui-color-text-light); + font-size: var(--go-ui-font-size-sm); + font-weight: var(--go-ui-font-weight-medium); + } + } + + .useful-links-content { + display: flex; + flex-wrap: wrap; + gap: var(--go-ui-spacing-xs) var(--go-ui-spacing-md); + } + } +} diff --git a/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/WfpAdam/EventListItem/index.tsx b/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/WfpAdam/EventListItem/index.tsx new file mode 100644 index 0000000..e11dc21 --- /dev/null +++ b/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/WfpAdam/EventListItem/index.tsx @@ -0,0 +1,68 @@ +import { + useEffect, + useRef, +} from 'react'; +import { TextOutput } from '@ifrc-go/ui'; + +import ImminentEventListItem from '#components/ImminentEventListItem'; +import type { RiskApiResponse } from '#utils/restRequest'; +import type { RiskEventListItemProps } from '#views/Preparedness/RiskAnalysis/RiskImminentEventMap'; + +type ImminentEventResponse = RiskApiResponse<'/api/v1/adam-exposure/'>; +type EventItem = NonNullable[number]; + +type Props = RiskEventListItemProps; + +function EventListItem(props: Props) { + const { + data: { + id, + publish_date, + title, + }, + expanded, + onExpandClick, + className, + children, + } = props; + + const elementRef = useRef(null); + + useEffect( + () => { + if (expanded && elementRef.current) { + const y = window.scrollY; + const x = window.scrollX; + elementRef.current.scrollIntoView({ + behavior: 'instant', + block: 'start', + }); + // NOTE: We need to scroll back because scrollIntoView also + // scrolls the parent container + window.scroll(x, y); + } + }, + [expanded], + ); + + return ( + + )} + > + {children} + + ); +} + +export default EventListItem; diff --git a/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/WfpAdam/index.tsx b/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/WfpAdam/index.tsx new file mode 100644 index 0000000..48fc8dd --- /dev/null +++ b/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/WfpAdam/index.tsx @@ -0,0 +1,235 @@ +import { useCallback } from 'react'; +import { numericIdSelector } from '@ifrc-go/ui/utils'; +import { + isDefined, + isNotDefined, +} from '@togglecorp/fujs'; +import { type LngLatBoundsLike } from 'mapbox-gl'; + +import { + type RiskApiResponse, + useRiskLazyRequest, + useRiskRequest, +} from '#utils/restRequest'; + +import { isValidFeatureCollection } from '../../../../../utils/risk'; +import RiskImminentEventMap, { type EventPointFeature } from '../../RiskImminentEventMap'; +import { + type RiskLayerProperties, + type RiskLayerSeverity, +} from '../../RiskImminentEventMap/utils'; +import EventDetails from './EventDetails'; +import EventListItem from './EventListItem'; + +type ImminentEventResponse = RiskApiResponse<'/api/v1/adam-exposure/'>; +type EventItem = NonNullable[number]; + +function hazardTypeSelector(item: EventItem) { + return item.hazard_type; +} + +function getLayerProperties( + feature: GeoJSON.Feature, +): RiskLayerProperties { + if (isNotDefined(feature) + || isNotDefined(feature.properties) + || isNotDefined(feature.geometry) + ) { + return { + type: 'unknown', + }; + } + + const geometryType = feature.geometry.type; + + if (geometryType === 'Point' || geometryType === 'MultiPoint') { + // FIXME: calculate isFuture + return { type: 'track-point', isFuture: true }; + } + + if (geometryType === 'LineString' || geometryType === 'MultiLineString') { + return { type: 'track-linestring' }; + } + + if (geometryType === 'Polygon' || geometryType === 'MultiPolygon') { + const alertLevel = feature.properties.alert_level; + + if (alertLevel === 'Cones') { + return { + type: 'uncertainty-cone', + forecastDays: undefined, + }; + } + + const severityMapping: Record = { + Red: 'red', + Orange: 'orange', + Green: 'green', + }; + + return { + type: 'exposure', + severity: severityMapping[alertLevel] ?? 'unknown', + }; + } + + return { + type: 'unknown', + }; +} + +type BaseProps = { + title: React.ReactNode; + bbox: LngLatBoundsLike | undefined; +} + +type Props = BaseProps & ({ + variant: 'global'; +} | { + variant: 'region'; + regionId: number; +} | { + variant: 'country'; + iso3: string; +}) + +function WfpAdam(props: Props) { + const { + title, + bbox, + variant, + } = props; + + const { + pending: pendingCountryRiskResponse, + response: countryRiskResponse, + } = useRiskRequest({ + apiType: 'risk', + // eslint-disable-next-line react/destructuring-assignment + skip: (variant === 'region' && isNotDefined(props.regionId)) + // eslint-disable-next-line react/destructuring-assignment + || (variant === 'country' && isNotDefined(props.iso3)), + url: '/api/v1/adam-exposure/', + query: { + limit: 9999, + // eslint-disable-next-line react/destructuring-assignment + iso3: variant === 'country' ? props.iso3 : undefined, + // eslint-disable-next-line react/destructuring-assignment + region: variant === 'region' ? [props.regionId] : undefined, + }, + }); + + const { + response: exposureResponse, + pending: exposureResponsePending, + trigger: fetchExposure, + } = useRiskLazyRequest<'/api/v1/adam-exposure/{id}/exposure/', { + eventId: number | string, + }>({ + apiType: 'risk', + url: '/api/v1/adam-exposure/{id}/exposure/', + pathVariables: ({ eventId }) => ({ id: Number(eventId) }), + }); + + const pointFeatureSelector = useCallback( + (event: EventItem): EventPointFeature | undefined => { + const { + id, + event_details, + hazard_type, + } = event; + const details = event_details as { latitude?: number; longitude?: number } | undefined; + const latitude = details?.latitude; + const longitude = details?.longitude; + + if ( + isNotDefined(latitude) + || isNotDefined(longitude) + || isNotDefined(hazard_type) + ) { + return undefined; + } + + return { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [longitude, latitude], + }, + properties: { + id, + hazard_type, + }, + }; + }, + [], + ); + + const footprintSelector = useCallback( + (exposure: RiskApiResponse<'/api/v1/adam-exposure/{id}/exposure/'> | undefined) => { + if (isNotDefined(exposure)) { + return undefined; + } + + const { storm_position_geojson } = exposure; + + if (isNotDefined(storm_position_geojson)) { + return undefined; + } + + const stormPositions = isValidFeatureCollection(storm_position_geojson) + ? storm_position_geojson + : undefined; + + const geoJson: GeoJSON.FeatureCollection = { + type: 'FeatureCollection' as const, + features: [ + ...stormPositions?.features?.map( + (feature) => ({ + ...feature, + properties: { + ...feature.properties, + ...getLayerProperties(feature), + }, + }), + ) ?? [], + ].filter(isDefined), + }; + + return geoJson; + }, + [], + ); + + const handleActiveEventChange = useCallback( + (eventId: number | undefined) => { + if (isDefined(eventId)) { + fetchExposure({ eventId }); + } else { + fetchExposure(undefined); + } + }, + [fetchExposure], + ); + + return ( + + ); +} + +export default WfpAdam; diff --git a/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/index.tsx b/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/index.tsx new file mode 100644 index 0000000..7d9cc25 --- /dev/null +++ b/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/index.tsx @@ -0,0 +1,288 @@ +import { + useCallback, + useState, +} from 'react'; +import { + CycloneIcon, + DroughtIcon, + EarthquakeIcon, + FloodIcon, + ForestFireIcon, +} from '@ifrc-go/icons'; +import { + Container, + InfoPopup, + LegendItem, + ListView, + Radio, +} from '@ifrc-go/ui'; +import { resolveToComponent } from '@ifrc-go/ui/utils'; +import type { LngLatBoundsLike } from 'mapbox-gl'; + +import Link from '#components/Link'; +import WikiLink from '#components/WikiLink'; +import { environment } from '#config'; +import { type components } from '#generated/riskTypes'; +import { hazardTypeToColorMap } from '#utils/risk'; + +import Gdacs from './Gdacs'; +import MeteoSwiss from './MeteoSwiss'; +import Pdc from './Pdc'; +import WfpAdam from './WfpAdam'; + +import styles from './styles.module.css'; + +export type ImminentEventSource = 'pdc' | 'wfpAdam' | 'gdacs' | 'meteoSwiss'; +type HazardType = components['schemas']['CommonHazardTypeEnumKey']; + +type BaseProps = { + className?: string; + title: React.ReactNode; + bbox: LngLatBoundsLike | undefined; + defaultSource?: ImminentEventSource; +} + +type Props = BaseProps & ({ + variant: 'global'; +} | { + variant: 'region'; + regionId: number; +} | { + variant: 'country'; + iso3: string; +}) + +function RiskImminentEvents(props: Props) { + const { + className, + defaultSource = 'gdacs', + ...otherProps + } = props; + const [activeView, setActiveView] = useState(defaultSource); + + const handleRadioClick = useCallback((key: ImminentEventSource) => { + setActiveView(key); + }, []); + + const riskHazards: Array<{ + key: HazardType, + label: string, + icon: React.ReactNode, + }> = [ + { + key: 'FL', + label: 'Flood', + icon: , + }, + { + key: 'TC', + label: 'Storm', + icon: , + }, + { + key: 'EQ', + label: 'Earthquake', + icon: , + }, + { + key: 'DR', + label: 'Drought', + icon: , + }, + { + key: 'WF', + label: 'Wildfire', + icon: , + }, + ]; + + return ( + + )} + footer={( + + + {riskHazards.map((hazard) => ( + + ))} + + + + here + + ), + }, + )} + /> + )} + > + GDACS + + + here + + ), + }, + )} + /> + )} + > + PDC + + {environment !== 'production' && ( + + here + + ), + }, + )} + /> + )} + > + WFP ADAM + + )} + {environment !== 'production' && ( + +
+ This impact estimates are produced by + MeteoSwiss HydroMet Impact Outlook. + © 2022 MeteoSwiss. All Rights reserved. +
+
+ {resolveToComponent( + 'Disclaimer: HydroMet Impact Outlook is in a pilot phase. MeteoSwiss makes no warranty with respect to the correctness or completeness of this information. This information does not replace the advice and guidance provided by the official meteorological and hydrological services for these regions. For further information click {here}.', + { + here: ( + + here + + ), + }, + )} +
+
+ )} + /> + )} + > + MeteoSwiss + + )} +
+ + )} + > + {activeView === 'pdc' && ( + + )} + {activeView === 'wfpAdam' && ( + + )} + {activeView === 'gdacs' && ( + + )} + {activeView === 'meteoSwiss' && ( + + )} +
+ ); +} + +export default RiskImminentEvents; diff --git a/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/styles.module.css b/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/styles.module.css new file mode 100644 index 0000000..cd40d93 --- /dev/null +++ b/app/views/Preparedness/RiskAnalysis/RiskImminentEvents/styles.module.css @@ -0,0 +1,5 @@ +.legend-icon { + > div { + padding: var(--go-ui-spacing-4xs) !important; + } +} \ No newline at end of file diff --git a/app/views/Preparedness/RiskAnalysis/RiskTable/index.tsx b/app/views/Preparedness/RiskAnalysis/RiskTable/index.tsx new file mode 100644 index 0000000..8b385d6 --- /dev/null +++ b/app/views/Preparedness/RiskAnalysis/RiskTable/index.tsx @@ -0,0 +1,269 @@ +import { + useCallback, + useMemo, +} from 'react'; +import { Table } from '@ifrc-go/ui'; +import { + createNumberColumn, + createStringColumn, + DEFAULT_INVALID_TEXT, + minSafe, + resolveToComponent, +} from '@ifrc-go/ui/utils'; +import { + _cs, + isDefined, + isNotDefined, + listToMap, + unique, +} from '@togglecorp/fujs'; + +import Link from '#components/Link'; +import { + CATEGORY_RISK_HIGH, + CATEGORY_RISK_LOW, + CATEGORY_RISK_MEDIUM, + CATEGORY_RISK_VERY_HIGH, + CATEGORY_RISK_VERY_LOW, +} from '#utils/constants'; +import { type RiskApiResponse } from '#utils/restRequest'; +import { + getDataWithTruthyHazardType, + getFiRiskDataItem, + getValueForSelectedMonths, + getWfRiskDataItem, + hasSomeDefinedValue, + riskScoreToCategory, +} from '#utils/risk'; + +import styles from './styles.module.css'; + +type CountryRiskResponse = RiskApiResponse<'/api/v1/country-seasonal/'>; +type RiskData = CountryRiskResponse[number]; + +interface Props { + className?: string; + riskData: RiskData | undefined; + selectedMonths: Record | undefined; + dataPending: boolean; +} + +function RiskTable(props: Props) { + const { + riskData, + className, + selectedMonths, + dataPending, + } = props; + + const fiData = useMemo( + () => getFiRiskDataItem(riskData?.ipc_displacement_data), + [riskData], + ); + + const wfData = useMemo( + () => getWfRiskDataItem(riskData?.gwis), + [riskData], + ); + + const hazardTypeList = useMemo( + () => ( + unique( + [ + ...riskData?.idmc?.filter(hasSomeDefinedValue) ?? [], + ...riskData?.raster_displacement_data?.filter(hasSomeDefinedValue) ?? [], + ...riskData?.inform_seasonal?.filter(hasSomeDefinedValue) ?? [], + fiData, + wfData, + ].filter(isDefined).map(getDataWithTruthyHazardType).filter(isDefined), + (data) => data.hazard_type, + ).map((combinedData) => ({ + hazard_type: combinedData.hazard_type, + hazard_type_display: combinedData.hazard_type_display, + })) + ), + [riskData, fiData, wfData], + ); + + type HazardTypeOption = (typeof hazardTypeList)[number]; + + const hazardKeySelector = useCallback( + (d: HazardTypeOption) => d.hazard_type, + [], + ); + + const riskScoreToLabel = useCallback( + (score: number | undefined | null, hazardType: HazardTypeOption['hazard_type']) => { + if (isNotDefined(score) || score < 0) { + return DEFAULT_INVALID_TEXT; + } + + const riskCategory = riskScoreToCategory(score, hazardType); + + if (isNotDefined(riskCategory)) { + return 'N/A'; + } + + const riskCategoryToLabelMap = { + [CATEGORY_RISK_VERY_HIGH]: 'Very high', + [CATEGORY_RISK_HIGH]: 'High', + [CATEGORY_RISK_MEDIUM]: 'Medium', + [CATEGORY_RISK_LOW]: 'Low', + [CATEGORY_RISK_VERY_LOW]: 'Very low', + }; + + return riskCategoryToLabelMap[riskCategory]; + }, + [], + ); + + const displacementRiskData = useMemo( + () => listToMap( + riskData?.idmc?.map(getDataWithTruthyHazardType).filter(isDefined) ?? [], + (data) => data.hazard_type, + ), + [riskData], + ); + + const exposureRiskData = useMemo( + () => ({ + ...listToMap( + riskData?.raster_displacement_data?.map( + getDataWithTruthyHazardType, + ).filter(isDefined) ?? [], + (data) => data.hazard_type, + ), + FI: fiData, + }), + [riskData, fiData], + ); + + const informSeasonalRiskData = useMemo( + () => ({ + ...listToMap( + riskData?.inform_seasonal?.map(getDataWithTruthyHazardType).filter(isDefined) ?? [], + (data) => data.hazard_type, + ), + WF: wfData, + }), + [wfData, riskData], + ); + + const riskTableColumns = useMemo( + () => ([ + createStringColumn( + 'hazard_type', + 'Hazard Type', + (item) => item.hazard_type_display, + ), + createStringColumn( + 'riskScore', + 'Risk Score', + (option) => riskScoreToLabel( + getValueForSelectedMonths( + selectedMonths, + informSeasonalRiskData[option.hazard_type], + 'max', + ), + option.hazard_type, + ), + { + headerInfoTitle: 'Risk Score', + // FIXME: add description for wildfire + headerInfoDescription: ( +
+
+ These figures depict INFORM seasonal + hazard exposure values for each country + for each month on a five-point scale: +
+
+ 1: Very Low | 2: Low | 3: Medium | 4: High | 5: Very High. +
+
+ {resolveToComponent( + 'More information on these values can be found {moreInfoLink}', + { + moreInfoLink: ( + + here + + ), + }, + )} +
+
+ ), + }, + ), + createNumberColumn( + 'exposure', + 'People Exposed', + (option) => getValueForSelectedMonths( + selectedMonths, + exposureRiskData[option.hazard_type], + option.hazard_type === 'FI' ? 'max' : 'sum', + ), + { + headerInfoTitle: 'People Exposed', + headerInfoDescription: 'These figures represent the number of people exposed to each hazard per month, on average. The population exposure figures are from the 2015 UNDRR Global Risk Model, based on average annual exposure to each hazard. The average annual exposure estimates were disaggregated by month based on recorded impacts of observed hazard events.', + maximumFractionDigits: 0, + }, + ), + createNumberColumn( + 'displacement', + 'People at Risk of Displacement', + (option) => { + // NOTE: Naturally displacement should always be greater than + // or equal to the exposure. To follow that logic we reduce + // displacement value to show the exposure in case displacement + // is greater than exposure + + const exposure = getValueForSelectedMonths( + selectedMonths, + exposureRiskData[option.hazard_type], + ); + + const displacement = getValueForSelectedMonths( + selectedMonths, + displacementRiskData[option.hazard_type], + ); + + if (isNotDefined(displacement)) { + return undefined; + } + + return minSafe([exposure, displacement]); + }, + { + headerInfoTitle: 'People at Risk of Displacement', + headerInfoDescription: "These figures represent the number of people expected to be displaced per month, on average, by each hazard. The estimates are based on the Internal Displacement Monitoring Centre's disaster displacement risk model using estimates for average annual displacement risk. These values were disaggregated by month based on historical displacement data associated with each hazard.", + maximumFractionDigits: 0, + }, + ), + ]), + [ + displacementRiskData, + exposureRiskData, + informSeasonalRiskData, + riskScoreToLabel, + selectedMonths, + ], + ); + + return ( +
+ ); +} + +export default RiskTable; diff --git a/app/views/Preparedness/RiskAnalysis/RiskTable/styles.module.css b/app/views/Preparedness/RiskAnalysis/RiskTable/styles.module.css new file mode 100644 index 0000000..901bb50 --- /dev/null +++ b/app/views/Preparedness/RiskAnalysis/RiskTable/styles.module.css @@ -0,0 +1,5 @@ +.inform-description { + display: flex; + flex-direction: column; + gap: var(--go-ui-spacing-md); +} diff --git a/app/views/Preparedness/RiskAnalysis/index.tsx b/app/views/Preparedness/RiskAnalysis/index.tsx new file mode 100644 index 0000000..b832fe9 --- /dev/null +++ b/app/views/Preparedness/RiskAnalysis/index.tsx @@ -0,0 +1,234 @@ +import { + use, + useMemo, +} from 'react'; +import { + Container, + Description, + ListView, +} from '@ifrc-go/ui'; +import { + isDefined, + isNotDefined, + mapToList, +} from '@togglecorp/fujs'; + +import Link from '#components/Link'; +import WikiLink from '#components/WikiLink'; +import CountryContext from '#contexts/GoContext'; +import useInputState from '#hooks/useInputState'; +import { multiMonthSelectDefaultValue } from '#utils/constants'; +import { getGeoJsonBounds } from '#utils/geo'; +import { useRiskRequest } from '#utils/restRequest'; + +import CountryRiskSourcesOutput from './CountryRiskSourcesOutput'; +import MultiMonthSelectInput from './MultiMonthSelectInput'; +import PossibleEarlyActionTable from './PossibleEarlyActionTable'; +import ReturnPeriodTable from './ReturnPeriodTable'; +import RiskBarChart from './RiskBarChart'; +import RiskImminentEvents, { type ImminentEventSource } from './RiskImminentEvents'; +import RiskTable from './RiskTable'; + +import styles from './styles.module.css'; + +function getCurrentMonth() { + return new Date().getMonth(); +} + +function RiskAnalysis() { + const { countryResponse, countryId } = use(CountryContext); + const [ + selectedMonths, + setSelectedMonths, + ] = useInputState<(typeof multiMonthSelectDefaultValue) | undefined>({ + ...multiMonthSelectDefaultValue, + [getCurrentMonth()]: true, + }); + + const { + pending: pendingCountryRiskResponse, + response: countryRiskResponse, + } = useRiskRequest({ + apiType: 'risk', + url: '/api/v1/country-seasonal/', + query: { + // FIXME: why do we need to use lowercase? + iso3: countryResponse?.iso3?.toLowerCase(), + }, + }); + + const { + pending: pendingImminentEventCounts, + response: imminentEventCountsResponse, + } = useRiskRequest({ + apiType: 'risk', + url: '/api/v1/country-imminent-counts/', + query: { + iso3: countryResponse?.iso3?.toLowerCase(), + }, + }); + + const hasImminentEvents = useMemo( + () => { + if (isNotDefined(imminentEventCountsResponse)) { + return false; + } + + const eventCounts = mapToList( + imminentEventCountsResponse, + (value) => value, + ).filter(isDefined).filter( + (value) => value > 0, + ); + + return eventCounts.length > 0; + }, + [imminentEventCountsResponse], + ); + + const defaultImminentEventSource = useMemo( + () => { + if (isNotDefined(imminentEventCountsResponse)) { + return undefined; + } + + const { + pdc, + adam, + gdacs, + meteoswiss, + } = imminentEventCountsResponse; + + if (isDefined(pdc) && pdc > 0) { + return 'pdc'; + } + + if (isDefined(adam) && adam > 0) { + return 'wfpAdam'; + } + + if (isDefined(gdacs) && gdacs > 0) { + return 'gdacs'; + } + + if (isDefined(meteoswiss) && meteoswiss > 0) { + return 'meteoSwiss'; + } + + return undefined; + }, + [imminentEventCountsResponse], + ); + + // NOTE: we always get 1 child in the response + const riskResponse = countryRiskResponse?.[0]; + const bbox = useMemo(() => ( + (countryResponse && countryResponse.bbox) + ? getGeoJsonBounds(countryResponse.bbox) + : undefined + ), [countryResponse]); + + return ( + + The following dataset displays information about the + modeled impact of specific forecasted or detected natural hazards. + + )} + headerActions={( + + )} + > + + {hasImminentEvents + && isDefined(countryResponse) + && isDefined(countryResponse.iso3) + && ( + + )} + + The table below displays available information about specific + disaster risks for each month. When you move the slider + from month to month, the information will update automatically. + Hold Shift to select a range of months — this will display the + cumulative number of people exposed and at risk of displacement. + Selecting Yearly Avg will display the annual figures from + INFORM and the total number of people exposed and at + risk of being displaced per country per year. + + )} + withHeaderBorder + footerActions={} + > + + + + + + + Download the EAP + + )} + spacing="lg" + withShadow + withPadding + withBackground + > + EAPs are a formal plan that guide timely and effective implementation + of early actions for extreme weather events, based on pre-agreed triggers. + + + + + + + ); +} + +export default RiskAnalysis; diff --git a/app/views/Preparedness/RiskAnalysis/styles.module.css b/app/views/Preparedness/RiskAnalysis/styles.module.css new file mode 100644 index 0000000..33dfb32 --- /dev/null +++ b/app/views/Preparedness/RiskAnalysis/styles.module.css @@ -0,0 +1,4 @@ +.eap-container { + align-self: center; + max-width: 40rem; +} diff --git a/app/views/Preparedness/index.tsx b/app/views/Preparedness/index.tsx new file mode 100644 index 0000000..2071406 --- /dev/null +++ b/app/views/Preparedness/index.tsx @@ -0,0 +1,52 @@ +import { Outlet } from 'react-router'; +import { + ListView, + NavigationTabList, +} from '@ifrc-go/ui'; + +import NavigationTab from '#components/NavigationTab'; +import Page from '#components/Page'; + +import AllEmergency from './AllEmergencies'; + +function Preparedness() { + return ( + + + + + + Emergency Alert + + + Disaster Response + {' '} + + + PMER + + + Risk Analysis + + + + + + ); +} + +export default Preparedness; diff --git a/app/views/PrivateLayout/index.tsx b/app/views/PrivateLayout/index.tsx index c8f0415..8465672 100644 --- a/app/views/PrivateLayout/index.tsx +++ b/app/views/PrivateLayout/index.tsx @@ -1,10 +1,8 @@ -import { Outlet } from "react-router"; +import { Outlet } from 'react-router'; function PrivateLayout() { return ( - <> - - + ); } diff --git a/app/views/PublicLayout/index.tsx b/app/views/PublicLayout/index.tsx index 75c8674..6d1a082 100644 --- a/app/views/PublicLayout/index.tsx +++ b/app/views/PublicLayout/index.tsx @@ -1,6 +1,8 @@ +import { Outlet } from 'react-router'; + function PublicLayout() { return ( -
+ ); } diff --git a/app/views/RootLayout/index.tsx b/app/views/RootLayout/index.tsx index 9e4b680..b23d3ce 100644 --- a/app/views/RootLayout/index.tsx +++ b/app/views/RootLayout/index.tsx @@ -1,31 +1,27 @@ -// import { -// use, -// useEffect, -// useState, -// } from 'react'; import { use, useEffect, - useState -} from 'react' + useMemo, +} from 'react'; import { Outlet } from 'react-router'; import { isDefined } from '@togglecorp/fujs'; import { gql } from 'urql'; +import GlobalFooter from '#components/Footer'; +import Navbar from '#components/Navbar'; +import { api } from '#config'; +import GoContext from '#contexts/GoContext'; import UserContext from '#contexts/UserContext'; -// import { isDefined } from '@togglecorp/fujs'; -// import { gql } from 'urql'; -// import PreloadMessage from '#components/PreloadMessage'; -// import UserContext from '#contexts/UserContext'; import { useMeQuery } from '#generated/types/graphql'; +import { useRequest } from '#utils/restRequest'; import styles from './styles.module.css'; -// const fetchHealth = fetch(`${import.meta.env.APP_GRAPHQL_ENDPOINT}/health-check/?format=json`, { -// method: 'GET', -// credentials: 'include', -// }) -// .then((res) => res.json()); +const fetchHealth = fetch(`${api}/health-check/?format=json`, { + method: 'GET', + credentials: 'include', +}) + .then((res) => res.json()); // eslint-disable-next-line @typescript-eslint/no-unused-vars const ME_QUERY = gql` @@ -42,47 +38,78 @@ const ME_QUERY = gql` } } `; +const countryId = 65; // ethiopia function RootLayout() { + use(fetchHealth); const { setUser } = use(UserContext); - const [ready, setReady] = useState(false); + const [{ fetching, data }] = useMeQuery(); - // const healthCheck = use(fetchHealth); + const { + pending: countryResponsePending, + response: countryResponse, + } = useRequest({ + url: '/api/v2/country/{id}/', + preserveResponse: true, + pathVariables: { + id: Number(countryId), + }, + }); - const [{ fetching, data }] = useMeQuery(); + const { + response: disasterTypes, + pending: disasterTypesPending, + } = useRequest( + { + url: '/api/v2/disaster_type/', + preserveResponse: true, + }, + ); + + const { + response: globalEnums, + pending: globalEnumsPending, + } = useRequest({ + url: '/api/v2/global-enums/', + preserveResponse: true, + }); + + const GoContextValue = useMemo(() => ({ + countryId, + countryResponse, + countryResponsePending, + disasterTypes, + disasterTypesPending, + globalEnums, + globalEnumsPending, + }), [ + countryResponse, + countryResponsePending, + disasterTypesPending, + disasterTypes, + globalEnums, + globalEnumsPending, + ]); useEffect(() => { - if ( fetching) { + if (fetching) { return; } if (isDefined(data?.me)) { - const fullName = data.me.fullName || ''; - const [firstName, ...lastNameParts] = fullName.split(' '); - const lastName = lastNameParts.join(' '); - setUser({ - ...data.me, - firstName, - lastName, - }); - } else { - setUser(undefined); + setUser(data.me); } + }, [fetching, data, setUser]); - // eslint-disable-next-line react-hooks/set-state-in-effect - setReady(true); - }, [ fetching, data, setUser]); - - if (!ready) { - return ( -
- Checking user session... -
- ); - } return ( -
- -
+ +
+ +
+ +
+ +
+
); } diff --git a/app/views/RootLayout/styles.module.css b/app/views/RootLayout/styles.module.css index abe04d7..e975806 100644 --- a/app/views/RootLayout/styles.module.css +++ b/app/views/RootLayout/styles.module.css @@ -3,5 +3,8 @@ position: relative; flex-direction: column; height: 100vh; - overflow: hidden; + + .page-content { + flex-grow: 1; + } } diff --git a/app/views/TeamList/Members/index.tsx b/app/views/TeamList/Members/index.tsx new file mode 100644 index 0000000..e95f700 --- /dev/null +++ b/app/views/TeamList/Members/index.tsx @@ -0,0 +1,259 @@ +import { useParams } from 'react-router'; +import { + DownloadTwoFillIcon, + SearchLineIcon, +} from '@ifrc-go/icons'; +import { + Button, + Container, + ListView, + Pager, + SelectInput, + Table, + TextInput, +} from '@ifrc-go/ui'; +import { SortContext } from '@ifrc-go/ui/contexts'; +import { + createElementColumn, + createStringColumn, +} from '@ifrc-go/ui/utils'; +import { gql } from 'urql'; + +import Link from '#components/Link'; +import Page from '#components/Page'; +import { + type TeamMembersQuery, + useTeamMembersQuery, +} from '#generated/types/graphql'; +import useFilterState from '#hooks/useFilterState'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const TEAM_MEMBERS_QUERY = gql` + query TeamMembers( + $offset: Int + $limit: Int + $search: String + $woredaId: ID + $teamId: ID! + $regionId: ID + ) { + team(id: $teamId) { + id + name + description + } + teamMembers( + pagination: { limit: $limit, offset: $offset } + filters: { + search: $search + woredaId: $woredaId + teamId: $teamId + regionId: $regionId + } + ) { + totalCount + results { + woredaId + updatedAt + training + teamId + sex + regionId + position + phoneNumber + order + name + id + fieldOfStudy + email + createdAt + } + } + } +`; +function idSelector(item: { id: T }) { + return item.id; +} + +type MemberList = NonNullable[number]; + +function NameEmailCell({ + fullName, + email, +}: { + fullName?: string | null; + email?: string | null; +}) { + return ( + + {fullName} + + {email} + + + ); +} + +function Members() { + const { id } = useParams<{ id: string }>(); + const { + sortState, + limit, + page, + setPage, + offset, + filter, + setFilterField, + rawFilter, + } = useFilterState<{ + searchText?: string, + sex?:string + }>({ + filter: {}, + pageSize: 15, + }); + + const [{ fetching, data }] = useTeamMembersQuery({ + variables: { + teamId: id!, + offset, + limit, + search: filter.searchText, + }, + pause: !id, + }); + + const members = data?.teamMembers.results ?? []; + + const columns = [ + createStringColumn( + 'sn', + 'S.N.', + (member) => String(members.indexOf(member) + 1), + { columnWidth: 20 }, + ), + createElementColumn< + MemberList, + string | number, + { fullName?: string | null; email?: string | null } + >( + 'name', + 'Full Name', + NameEmailCell, + (_, member) => ({ + fullName: member?.name, + email: member?.email, + }), + ), + createStringColumn( + 'sex', + 'Gender', + (dept) => dept?.sex?.toString(), + ), + createStringColumn( + 'phoneNumber', + 'Number', + (dept) => dept?.phoneNumber, + ), + createStringColumn( + 'position', + 'Position', + (dept) => dept?.position, + ), + createStringColumn( + 'training', + 'Training', + (dept) => dept?.training, + ), + createStringColumn( + 'fieldOfStudy', + 'Field of study', + (dept) => dept?.fieldOfStudy, + ), + ]; + + // NOTE: the value represents gender enum in query + const genderOptions = [ + { + key: 'MALE', + label: 'Male', + value: 10, + }, + { + key: 'FEMALE', + label: 'Female', + value: 20, + }, + { + key: 'OTHER', + label: 'Other', + value: 30, + }, + ]; + + return ( + + {members?.length} + {' '} + Members + + )} + > + } + > + Export + + )} + filters={( + <> + option.key} + labelSelector={(option) => option.label} + value={filter.sex} + onChange={setFilterField} + /> + } + /> + + )} + footerActions={( + + )} + > + +
+ + + + ); +} + +export default Members; diff --git a/app/views/TeamList/index.tsx b/app/views/TeamList/index.tsx new file mode 100644 index 0000000..fd9ec1b --- /dev/null +++ b/app/views/TeamList/index.tsx @@ -0,0 +1,140 @@ +import { SearchLineIcon } from '@ifrc-go/icons'; +import { + Container, + Pager, + Table, + TextInput, +} from '@ifrc-go/ui'; +import { + createElementColumn, + createStringColumn, +} from '@ifrc-go/ui/utils'; +import { gql } from 'urql'; + +import Link from '#components/Link'; +import Page from '#components/Page'; +import { + type TeamsQuery, + useTeamsQuery, +} from '#generated/types/graphql'; +import useFilterState from '#hooks/useFilterState'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const TEAMS_QUERY = gql` + query Teams( + $limit: Int = 10 + $offset: Int = 0 + $search: String = "" + ) { + teams( + pagination: { limit: $limit, offset: $offset } + filters: { search: $search } + ) { + totalCount + results { + id + name + } + } + } +`; +const teamKeySelector = (item: TeamList) => item.id; + +type TeamList = NonNullable[number]; + +function TeamActions({ id }: {id: string}) { + return ( + + View Members + + ); +} + +function TeamList() { + const { + limit, + page, + rawFilter, + filter, + setFilterField, + setPage, + offset, + } = useFilterState<{ + searchText?: string + }>({ + filter: {}, + pageSize: 6, + }); + const [{ data, fetching }] = useTeamsQuery(({ + variables: { + search: filter.searchText, + limit, + offset, + }, + })); + const teams = data?.teams.results ?? []; + + const columns = [ + createStringColumn( + 'sn', + 'S.N.', + (item) => String(teams.indexOf(item) + 1), + { columnWidth: 20 }, + ), + createStringColumn( + 'name', + 'Team Name', + (dept) => dept?.name, + ), + createElementColumn( + 'action', + 'Actions', + TeamActions, + (_key, item) => ({ + id: item.id, + }), + ), + ]; + return ( + + } + /> + )} + footerActions={( + + )} + empty={teams.length === 0} + > +
+ + + ); +} + +export default TeamList; diff --git a/backend b/backend index 14996ec..a723da9 160000 --- a/backend +++ b/backend @@ -1 +1 @@ -Subproject commit 14996ec631caccb48531e91c76b2a14a3eda8dad +Subproject commit a723da9677d808d1fbb0b4f5b1f01629ec6880a5 diff --git a/env.ts b/env.ts new file mode 100644 index 0000000..a94f532 --- /dev/null +++ b/env.ts @@ -0,0 +1,42 @@ +import { + defineConfig, + overrideDefineForWebAppServe, + Schema, +} from '@togglecorp/vite-plugin-validate-env'; + +const webAppServeEnabled = process.env.WEB_APP_SERVE_ENABLED?.toLowerCase() === 'true'; +if (webAppServeEnabled) { + // eslint-disable-next-line no-console + console.warn('Building application for web-app-serve'); +} +const overrideDefine = webAppServeEnabled + ? overrideDefineForWebAppServe + : undefined; + +export default defineConfig({ + overrideDefine, + validator: 'builtin', + schema: { + APP_TITLE: Schema.string(), + APP_ENVIRONMENT: (key: string, value: string) => { + // NOTE: APP_ENVIRONMENT_PLACEHOLDER is meant to be used with image builds + // The value will be later replaced with the actual value + const regex = /^production|staging|testing|alpha-\d+|development|APP_ENVIRONMENT_PLACEHOLDER$/; + const valid = !!value && (value.match(regex) !== null); + if (!valid) { + throw new Error(`Value for environment variable "${key}" must match regex "${regex}", instead received "${value}"`); + } + if (value === 'APP_ENVIRONMENT_PLACEHOLDER') { + // eslint-disable-next-line no-console + console.warn(`Using ${value} for app environment. Make sure to not use this for builds without nginx-serve`); + } + return value as ('production' | 'staging' | 'testing' | `alpha-${number}` | 'development' | 'APP_ENVIRONMENT_PLACEHOLDER'); + }, + APP_GRAPHQL_CODEGEN_ENDPOINT: Schema.string(), + APP_GRAPHQL_ENDPOINT: Schema.string(), + APP_GO_URL: Schema.string.optional(), + APP_GO_API: Schema.string.optional(), + APP_GO_RISK_API_ENDPOINT: Schema.string.optional(), + APP_MAPBOX_TOKEN: Schema.string.optional(), + }, +}); \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js index 9e5bea3..86a3e61 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,143 +1,161 @@ -import js from "@eslint/js"; -import globals from "globals"; -import reactHooks from "eslint-plugin-react-hooks"; -import reactRefresh from "eslint-plugin-react-refresh"; -import tseslint from "typescript-eslint"; -import airbnb from "eslint-config-airbnb"; -import { defineConfig, globalIgnores } from "eslint/config"; -import importPlugin from 'eslint-plugin-import' -import simpleImportSort from "eslint-plugin-simple-import-sort"; -import importNewlines from "eslint-plugin-import-newlines"; -import reactPlugin from 'eslint-plugin-react' -import reactCompiler from "eslint-plugin-react-compiler"; - - -export default defineConfig([ - globalIgnores([ - "dist", - "node_modules/", - "build/", - "coverage/", - "codegen.ts", - "generated/types/", - ]), - { - files: ["app/**/*.tsx", "app/**/*.jsx", "app/**/*.ts", "app/**/*.js"], +import { FlatCompat } from '@eslint/eslintrc'; +import js from '@eslint/js'; +import json from "@eslint/json"; + +import process from 'process'; + +const dirname = process.cwd(); + +const compat = new FlatCompat({ + baseDirectory: dirname, + resolvePluginsRelativeTo: dirname, +}); + +const appConfigs = compat.config({ + env: { + node: true, + browser: true, + es2020: true, + }, + root: true, extends: [ - js.configs.recommended, - tseslint.configs.recommended, - reactHooks.configs.flat.recommended, - reactRefresh.configs.vite, + 'airbnb', + 'airbnb/hooks', + 'plugin:@typescript-eslint/recommended', + 'plugin:react-hooks/recommended', ], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - parserOptions: { - warnOnUnsupportedTypeScriptVersion: false, - }, - }, - plugins: { - 'simple-import-sort': simpleImportSort, - 'import-newlines': importNewlines, - 'import': importPlugin, - "airbnb": airbnb, - "react": reactPlugin, - "react-compiler": reactCompiler, + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', }, + plugins: [ + '@typescript-eslint', + 'react-refresh', + 'simple-import-sort', + 'import-newlines' + ], settings: { - "react": { - "version": "detect", - }, - "import/parsers": { - "@typescript-eslint/parser": [".ts", ".tsx"], - }, - "import/resolver": { - typescript: { - project: ["./tsconfig.json"], + 'import/parsers': { + '@typescript-eslint/parser': ['.ts', '.tsx'] + }, + 'import/resolver': { + typescript: { + project: [ + './tsconfig.json', + ], + }, }, - }, }, rules: { - "react-refresh/only-export-components": "warn", + 'react-refresh/only-export-components': 'warn', - "no-unused-vars": 0, - "@typescript-eslint/no-unused-vars": 1, + 'no-unused-vars': 0, + '@typescript-eslint/no-unused-vars': 1, - "no-use-before-define": 0, - "@typescript-eslint/no-use-before-define": 1, + 'no-use-before-define': 0, + '@typescript-eslint/no-use-before-define': 1, - "no-shadow": 0, - "@typescript-eslint/no-shadow": ["error"], + 'no-shadow': 0, + '@typescript-eslint/no-shadow': ['error'], - "import/no-extraneous-dependencies": [ - "error", - { - devDependencies: [ - "**/*.test.{ts,tsx}", - "eslint.config.js", - "postcss.config.cjs", - "stylelint.config.cjs", - "vite.config.ts", - ], - optionalDependencies: false, - }, - ], + 'import/no-extraneous-dependencies': [ + 'error', + { + devDependencies: [ + '**/*.test.{ts,tsx}', + 'eslint.config.js', + 'postcss.config.cjs', + 'stylelint.config.cjs', + 'vite.config.ts', + ], + optionalDependencies: false, + }, + ], + + indent: ['error', 4, { SwitchCase: 1 }], - indent: ["error", 4, { SwitchCase: 1 }], + 'import/no-cycle': ['error', { allowUnsafeDynamicCyclicDependency: true }], - "import/no-cycle": [ - "error", - { allowUnsafeDynamicCyclicDependency: true }, - ], + 'react/react-in-jsx-scope': 'off', + 'camelcase': 'off', - "react/react-in-jsx-scope": "off", - camelcase: "off", + 'react/jsx-indent': ['error', 4], + 'react/jsx-indent-props': ['error', 4], + 'react/jsx-filename-extension': ['error', { extensions: ['.js', '.jsx', '.ts', '.tsx'] }], - "react/jsx-indent": ["error", 4], - "react/jsx-indent-props": ["error", 4], - "react/jsx-filename-extension": [ - "error", - { extensions: [".js", ".jsx", ".ts", ".tsx"] }, - ], + 'import/extensions': ['off', 'never'], + 'import/named': 'warn', - "import/extensions": ["off", "never"], - "import/named": "warn", + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'warn', - "react-hooks/rules-of-hooks": "error", - "react-hooks/exhaustive-deps": "warn", + 'react/require-default-props': ['warn', { ignoreFunctionalComponents: true }], + 'simple-import-sort/imports': 'warn', + 'simple-import-sort/exports': 'warn', + 'import-newlines/enforce': ['warn', 1], - "react/require-default-props": [ - "warn", - { ignoreFunctionalComponents: true }, - ], - "simple-import-sort/imports": [ - "error", + 'react/jsx-props-no-spreading': 'warn' + }, + overrides: [ { - groups: [ - // Side effect imports - ["^\\u0000"], - // React related packages come first - ["^react", "^@?\\w"], - // Internal packages - ["^#.+$"], - // Parent imports, other relative imports - [ - "^\\.\\.(?!/?$)", - "^\\.\\./?$", - "^\\./(?=.*/)(?!/?$)", - "^\\.(?!/?$)", - "^\\./?$", - ], - // Style/asset imports - ["^.+\\.json$", "^.+\\.module.css$"], - ], - }, - ], - "simple-import-sort/exports": "warn", - "import-newlines/enforce": ["warn", 1], + files: ['*.js', '*.jsx', '*.ts', '*.tsx'], + rules: { + 'simple-import-sort/imports': [ + 'error', + { + 'groups': [ + // side effect imports + ['^\\u0000'], + // packages `react` related packages come first + ['^react', '^@?\\w'], + // internal packages + ['^#.+$'], + // parent imports. Put `..` last + // other relative imports. Put same-folder imports and `.` last + ['^\\.\\.(?!/?$)', '^\\.\\./?$', '^\\./(?=.*/)(?!/?$)', '^\\.(?!/?$)', '^\\./?$'], + // style imports + ['^.+\\.json$', '^.+\\.module.css$'], + ] + } + ] + } + } + ], +}).map((conf) => ({ + ...conf, + files: ['app/**/*.tsx', 'app/**/*.jsx', 'app/**/*.ts', 'app/**/*.js'], + ignores: [ + "node_modules/", + "build/", + "coverage/", + "codegen.ts", + 'generated/types/' + ], + +})); - "react/jsx-props-no-spreading": "warn", +const otherConfig = { + files: ['*.js', '*.cjs'], + ...js.configs.recommended +}; + +const jsonConfig = { + files: ['**/*.json'], + language: 'json/json', + rules: { + 'json/no-duplicate-keys': 'error', }, +}; + - } -]) +export default [ + { + plugins: { + json, + }, + }, + ...appConfigs, + otherConfig, + jsonConfig, +]; diff --git a/go-api b/go-api new file mode 160000 index 0000000..798960e --- /dev/null +++ b/go-api @@ -0,0 +1 @@ +Subproject commit 798960e777b6564a7430c3d1f7fa2a6708729fe0 diff --git a/go-risk-module-api b/go-risk-module-api new file mode 160000 index 0000000..3a9742d --- /dev/null +++ b/go-risk-module-api @@ -0,0 +1 @@ +Subproject commit 3a9742dfbc39a90744d9f72330675f512eead7ea diff --git a/index.html b/index.html index d79beee..1830d53 100644 --- a/index.html +++ b/index.html @@ -4,10 +4,45 @@ - Ethiopian Red Cross Society + %APP_TITLE% + + + + + + + -
- +
+ diff --git a/package.json b/package.json index 7757bc8..fa7037e 100644 --- a/package.json +++ b/package.json @@ -3,52 +3,82 @@ "private": true, "version": "0.0.0", "type": "module", + "packageManager": "pnpm@10.0.0", "scripts": { "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", "preview": "vite preview", - "generate:type": "graphql-codegen --require dotenv/config --config codegen.ts" + "prelint": "pnpm generate:type", + "lint:css": "stylelint \"./app/**/*.css\"", + "generate:type": "pnpm run generate:type:graphql && pnpm run generate:type:risk-api && pnpm run generate:type:go-api", + "generate:type:graphql": "graphql-codegen --require dotenv/config --config codegen.ts", + "generate:type:risk-api": "openapi-typescript go-risk-module-api/openapi-schema.yaml -o ./generated/riskTypes.ts --alphabetize", + "generate:type:go-api": "openapi-typescript go-api/assets/openapi-schema.yaml -o ./generated/types.ts --alphabetize", + "typecheck": "tsc" }, "dependencies": { "@graphql-codegen/cli": "^6.2.1", + "@graphql-eslint/eslint-plugin": "^4.4.0", "@ifrc-go/icons": "^2.0.1", "@ifrc-go/ui": "2.0.0-beta.2", + "@sentry/react": "^10.49.0", "@togglecorp/fujs": "^2.2.0", + "@togglecorp/re-map": "^0.3.0", + "@togglecorp/toggle-form": "^2.0.4", + "@togglecorp/toggle-request": "1.0.0-beta.3", + "@turf/bbox": "^7.0.0", + "@turf/buffer": "^7.0.0", "@urql/exchange-graphcache": "^9.0.0", + "file-saver": "^2.0.5", "graphql": "^16.13.2", "knip": "^6.3.0", + "lint": "^0.8.19", + "mapbox-gl": "^1.13.3", + "openapi-typescript": "^7.13.0", + "papaparse": "^5.5.3", + "powerbi-client": "^2.23.10", + "powerbi-client-react": "^2.0.2", "react": "^19.2.4", "react-cookie": "^8.1.0", "react-dom": "^19.2.4", + "react-pdf": "^10.4.1", "react-router": "^7.14.0", "urql": "^5.0.1" }, "devDependencies": { "@babel/core": "^7.29.0", + "@eslint/eslintrc": "^3.3.5", "@eslint/js": "^9.39.4", + "@eslint/json": "^1.2.0", "@graphql-codegen/introspection": "^5.0.1", "@graphql-codegen/typescript-urql": "^5.0.0", "@rolldown/plugin-babel": "^0.2.2", + "@stylistic/stylelint-plugin": "^5.1.0", + "@togglecorp/vite-plugin-validate-env": "^2.2.1", "@types/babel__core": "^7.20.5", + "@types/file-saver": "^2.0.7", + "@types/mapbox-gl": "^1.13.3", "@types/node": "^24.12.2", + "@types/papaparse": "^5.5.2", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", + "@typescript-eslint/eslint-plugin": "^8.49.0", + "@typescript-eslint/parser": "^8.54.0", "@vitejs/plugin-react": "^6.0.1", "autoprefixer": "^10.4.27", "babel-plugin-react-compiler": "^1.0.0", "dotenv-cli": "^11.0.0", "eslint": "^9.39.4", "eslint-config-airbnb": "^19.0.4", - "eslint-config-airbnb-flat": "^0.0.12", "eslint-import-resolver-typescript": "^4.4.4", "eslint-plugin-import": "^2.32.0", "eslint-plugin-import-exports-imports-resolver": "^1.0.1", "eslint-plugin-import-newlines": "^2.0.0", + "eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-react": "^7.37.5", - "eslint-plugin-react-compiler": "19.1.0-rc.2", "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.5.2", + "eslint-plugin-react-refresh": "^0.4.24", "eslint-plugin-simple-import-sort": "^13.0.0", "globals": "^17.4.0", "postcss": "^8.5.9", @@ -57,12 +87,13 @@ "postcss-preset-env": "^11.2.0", "stylelint": "^17.6.0", "stylelint-config-concentric": "^2.0.2", - "stylelint-config-recommended": "^18.0.0", + "stylelint-config-standard": "^40.0.0", "stylelint-no-unused-selectors": "^1.0.40", "stylelint-value-no-unknown-custom-properties": "^6.1.1", "typescript": "~6.0.2", "typescript-eslint": "^8.58.0", "vite": "^8.0.4", - "vite-tsconfig-paths": "^6.1.1" + "vite-plugin-checker": "^0.13.0", + "vite-plugin-svgr": "^5.2.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5128b86..46d00db 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,24 +11,66 @@ importers: '@graphql-codegen/cli': specifier: ^6.2.1 version: 6.2.1(@types/node@24.12.2)(graphql@16.13.2)(typescript@6.0.2) + '@graphql-eslint/eslint-plugin': + specifier: ^4.4.0 + version: 4.4.0(@types/node@24.12.2)(eslint@9.39.4(jiti@2.6.1))(graphql@16.13.2)(typescript@6.0.2) '@ifrc-go/icons': specifier: ^2.0.1 version: 2.0.1(react@19.2.4) '@ifrc-go/ui': specifier: 2.0.0-beta.2 version: 2.0.0-beta.2(@ifrc-go/icons@2.0.1(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@sentry/react': + specifier: ^10.49.0 + version: 10.49.0(react@19.2.4) '@togglecorp/fujs': specifier: ^2.2.0 version: 2.2.0 + '@togglecorp/re-map': + specifier: ^0.3.0 + version: 0.3.0(mapbox-gl@1.13.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@togglecorp/toggle-form': + specifier: ^2.0.4 + version: 2.0.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@togglecorp/toggle-request': + specifier: 1.0.0-beta.3 + version: 1.0.0-beta.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@turf/bbox': + specifier: ^7.0.0 + version: 7.3.5 + '@turf/buffer': + specifier: ^7.0.0 + version: 7.3.5 '@urql/exchange-graphcache': specifier: ^9.0.0 version: 9.0.0(@urql/core@6.0.1(graphql@16.13.2))(graphql@16.13.2) + file-saver: + specifier: ^2.0.5 + version: 2.0.5 graphql: specifier: ^16.13.2 version: 16.13.2 knip: specifier: ^6.3.0 version: 6.3.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + lint: + specifier: ^0.8.19 + version: 0.8.19 + mapbox-gl: + specifier: ^1.13.3 + version: 1.13.3 + openapi-typescript: + specifier: ^7.13.0 + version: 7.13.0(typescript@6.0.2) + papaparse: + specifier: ^5.5.3 + version: 5.5.3 + powerbi-client: + specifier: ^2.23.10 + version: 2.23.10 + powerbi-client-react: + specifier: ^2.0.2 + version: 2.0.2(react@19.2.4) react: specifier: ^19.2.4 version: 19.2.4 @@ -38,6 +80,9 @@ importers: react-dom: specifier: ^19.2.4 version: 19.2.4(react@19.2.4) + react-pdf: + specifier: ^10.4.1 + version: 10.4.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react-router: specifier: ^7.14.0 version: 7.14.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -48,9 +93,15 @@ importers: '@babel/core': specifier: ^7.29.0 version: 7.29.0 + '@eslint/eslintrc': + specifier: ^3.3.5 + version: 3.3.5 '@eslint/js': specifier: ^9.39.4 version: 9.39.4 + '@eslint/json': + specifier: ^1.2.0 + version: 1.2.0 '@graphql-codegen/introspection': specifier: ^5.0.1 version: 5.0.1(graphql@16.13.2) @@ -60,18 +111,39 @@ importers: '@rolldown/plugin-babel': specifier: ^0.2.2 version: 0.2.2(@babel/core@7.29.0)(@babel/runtime@7.29.2)(rolldown@1.0.0-rc.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2))(vite@8.0.5(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.2)(jiti@2.6.1)(yaml@2.8.3)) + '@stylistic/stylelint-plugin': + specifier: ^5.1.0 + version: 5.1.0(stylelint@17.6.0(typescript@6.0.2)) + '@togglecorp/vite-plugin-validate-env': + specifier: ^2.2.1 + version: 2.2.1(vite@8.0.5(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.2)(jiti@2.6.1)(yaml@2.8.3)) '@types/babel__core': specifier: ^7.20.5 version: 7.20.5 + '@types/file-saver': + specifier: ^2.0.7 + version: 2.0.7 + '@types/mapbox-gl': + specifier: ^1.13.3 + version: 1.13.10 '@types/node': specifier: ^24.12.2 version: 24.12.2 + '@types/papaparse': + specifier: ^5.5.2 + version: 5.5.2 '@types/react': specifier: ^19.2.14 version: 19.2.14 '@types/react-dom': specifier: ^19.2.3 version: 19.2.3(@types/react@19.2.14) + '@typescript-eslint/eslint-plugin': + specifier: ^8.49.0 + version: 8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/parser': + specifier: ^8.54.0 + version: 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2) '@vitejs/plugin-react': specifier: ^6.0.1 version: 6.0.1(@rolldown/plugin-babel@0.2.2(@babel/core@7.29.0)(@babel/runtime@7.29.2)(rolldown@1.0.0-rc.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2))(vite@8.0.5(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.2)(jiti@2.6.1)(yaml@2.8.3)))(babel-plugin-react-compiler@1.0.0)(vite@8.0.5(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.2)(jiti@2.6.1)(yaml@2.8.3)) @@ -90,9 +162,6 @@ importers: eslint-config-airbnb: specifier: ^19.0.4 version: 19.0.4(eslint-plugin-import@2.32.0)(eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.4(jiti@2.6.1)))(eslint-plugin-react-hooks@7.0.1(eslint@9.39.4(jiti@2.6.1)))(eslint-plugin-react@7.37.5(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) - eslint-config-airbnb-flat: - specifier: ^0.0.12 - version: 0.0.12(eslint-plugin-import@2.32.0)(typescript@6.0.2) eslint-import-resolver-typescript: specifier: ^4.4.4 version: 4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)) @@ -105,18 +174,18 @@ importers: eslint-plugin-import-newlines: specifier: ^2.0.0 version: 2.0.0(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-jsx-a11y: + specifier: ^6.10.2 + version: 6.10.2(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-react: specifier: ^7.37.5 version: 7.37.5(eslint@9.39.4(jiti@2.6.1)) - eslint-plugin-react-compiler: - specifier: 19.1.0-rc.2 - version: 19.1.0-rc.2(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-react-hooks: specifier: ^7.0.1 version: 7.0.1(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-react-refresh: - specifier: ^0.5.2 - version: 0.5.2(eslint@9.39.4(jiti@2.6.1)) + specifier: ^0.4.24 + version: 0.4.26(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-simple-import-sort: specifier: ^13.0.0 version: 13.0.0(eslint@9.39.4(jiti@2.6.1)) @@ -141,9 +210,9 @@ importers: stylelint-config-concentric: specifier: ^2.0.2 version: 2.0.2(stylelint@17.6.0(typescript@6.0.2)) - stylelint-config-recommended: - specifier: ^18.0.0 - version: 18.0.0(stylelint@17.6.0(typescript@6.0.2)) + stylelint-config-standard: + specifier: ^40.0.0 + version: 40.0.0(stylelint@17.6.0(typescript@6.0.2)) stylelint-no-unused-selectors: specifier: ^1.0.40 version: 1.0.40(stylelint@17.6.0(typescript@6.0.2)) @@ -159,9 +228,12 @@ importers: vite: specifier: ^8.0.4 version: 8.0.5(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.2)(jiti@2.6.1)(yaml@2.8.3) - vite-tsconfig-paths: - specifier: ^6.1.1 - version: 6.1.1(typescript@6.0.2)(vite@8.0.5(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.2)(jiti@2.6.1)(yaml@2.8.3)) + vite-plugin-checker: + specifier: ^0.13.0 + version: 0.13.0(eslint@9.39.4(jiti@2.6.1))(meow@14.1.0)(optionator@0.9.4)(stylelint@17.6.0(typescript@6.0.2))(typescript@6.0.2)(vite@8.0.5(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.2)(jiti@2.6.1)(yaml@2.8.3)) + vite-plugin-svgr: + specifier: ^5.2.0 + version: 5.2.0(typescript@6.0.2)(vite@8.0.5(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.2)(jiti@2.6.1)(yaml@2.8.3)) packages: @@ -194,28 +266,14 @@ packages: resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} engines: {node: '>=6.9.0'} - '@babel/helper-annotate-as-pure@7.27.3': - resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} - engines: {node: '>=6.9.0'} - '@babel/helper-compilation-targets@7.28.6': resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} engines: {node: '>=6.9.0'} - '@babel/helper-create-class-features-plugin@7.28.6': - resolution: {integrity: sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - '@babel/helper-globals@7.28.0': resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} engines: {node: '>=6.9.0'} - '@babel/helper-member-expression-to-functions@7.28.5': - resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==} - engines: {node: '>=6.9.0'} - '@babel/helper-module-imports@7.28.6': resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} engines: {node: '>=6.9.0'} @@ -226,24 +284,10 @@ packages: peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-optimise-call-expression@7.27.1': - resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} - engines: {node: '>=6.9.0'} - '@babel/helper-plugin-utils@7.28.6': resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} engines: {node: '>=6.9.0'} - '@babel/helper-replace-supers@7.28.6': - resolution: {integrity: sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-skip-transparent-expression-wrappers@7.27.1': - resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} - engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@7.27.1': resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} @@ -265,13 +309,6 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - '@babel/plugin-proposal-private-methods@7.18.6': - resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-import-assertions@7.28.6': resolution: {integrity: sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==} engines: {node: '>=6.9.0'} @@ -304,6 +341,10 @@ packages: '@cacheable/utils@2.4.1': resolution: {integrity: sha512-eiFgzCbIneyMlLOmNG4g9xzF7Hv3Mga4LjxjcSC/ues6VYq2+gUbQI8JqNuw/ZM8tJIeIaBGpswAsqV2V7ApgA==} + '@colors/colors@1.5.0': + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} + '@csstools/cascade-layer-name-parser@3.0.0': resolution: {integrity: sha512-/3iksyevwRfSJx5yH0RkcrcYXwuhMQx3Juqf40t97PeEy2/Mz2TItZ/z/216qpe4GgOyFBP8MKIwVvytzHmfIQ==} engines: {node: '>=20.19.0'} @@ -670,22 +711,22 @@ packages: resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/eslintrc@2.1.4': - resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@eslint/core@1.2.1': + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@eslint/eslintrc@3.3.5': resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@8.57.1': - resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@eslint/js@9.39.4': resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/json@1.2.0': + resolution: {integrity: sha512-CEFEyNgvzu8zn5QwVYDg3FaG+ZKUeUsNYitFpMYJAqoAlnw68EQgNbUfheSmexZr4n0wZPrAkPLuvsLaXO6wRw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@eslint/object-schema@2.1.7': resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -694,6 +735,10 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/plugin-kit@0.6.1': + resolution: {integrity: sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@fastify/busboy@3.2.0': resolution: {integrity: sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==} @@ -789,6 +834,20 @@ packages: peerDependencies: graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + '@graphql-eslint/eslint-plugin@4.4.0': + resolution: {integrity: sha512-dhW6fpk3Souuaphhc38uMAGCcgKMgtCJWFygIKODw/Kns43wiQqRPVay0aNFY1JBx3aevn4KPT/BCOdm6HNncA==} + engines: {node: '>=18'} + peerDependencies: + '@apollo/subgraph': ^2 + eslint: '>=8.44.0' + graphql: ^16 + json-schema-to-ts: ^3 + peerDependenciesMeta: + '@apollo/subgraph': + optional: true + json-schema-to-ts: + optional: true + '@graphql-hive/signal@2.0.0': resolution: {integrity: sha512-Pz8wB3K0iU6ae9S1fWfsmJX24CcGeTo6hE7T44ucmV/ALKRj+bxClmqrYcDT7v3f0d12Rh4FAXBb6gon+WkDpQ==} engines: {node: '>=20.0.0'} @@ -925,6 +984,12 @@ packages: peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@graphql-tools/utils@10.11.0': + resolution: {integrity: sha512-iBFR9GXIs0gCD+yc3hoNswViL1O5josI33dUqiNStFI/MHLCEPduasceAcazRH77YONKNiviHBV8f7OgcT4o2Q==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@graphql-tools/utils@11.0.0': resolution: {integrity: sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA==} engines: {node: '>=16.0.0'} @@ -950,18 +1015,13 @@ packages: resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} engines: {node: '>=18.18.0'} - '@humanwhocodes/config-array@0.13.0': - resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} - engines: {node: '>=10.10.0'} - deprecated: Use @eslint/config-array instead - '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} - '@humanwhocodes/object-schema@2.0.3': - resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} - deprecated: Use @eslint/object-schema instead + '@humanwhocodes/momoa@3.3.10': + resolution: {integrity: sha512-KWiFQpSAqEIyrTXko3hFNLeQvSK8zXlJQzhhxsyVn58WFRYXST99b3Nqnu+ttOtjds2Pl2grUHGpe2NzhPynuQ==} + engines: {node: '>=18'} '@humanwhocodes/retry@0.4.3': resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} @@ -1138,6 +1198,114 @@ packages: '@keyv/serialize@1.1.1': resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + '@kwsites/file-exists@1.1.1': + resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==} + + '@kwsites/promise-deferred@1.1.1': + resolution: {integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==} + + '@mapbox/geojson-rewind@0.5.2': + resolution: {integrity: sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==} + hasBin: true + + '@mapbox/geojson-types@1.0.2': + resolution: {integrity: sha512-e9EBqHHv3EORHrSfbR9DqecPNn+AmuAoQxV6aL8Xu30bJMJR1o8PZLZzpk1Wq7/NfCbuhmakHTPYRhoqLsXRnw==} + + '@mapbox/jsonlint-lines-primitives@2.0.2': + resolution: {integrity: sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==} + engines: {node: '>= 0.6'} + + '@mapbox/mapbox-gl-supported@1.5.0': + resolution: {integrity: sha512-/PT1P6DNf7vjEEiPkVIRJkvibbqWtqnyGaBz3nfRdcxclNSnSdaLU5tfAgcD7I8Yt5i+L19s406YLl1koLnLbg==} + peerDependencies: + mapbox-gl: '>=0.32.1 <2.0.0' + + '@mapbox/point-geometry@0.1.0': + resolution: {integrity: sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==} + + '@mapbox/tiny-sdf@1.2.5': + resolution: {integrity: sha512-cD8A/zJlm6fdJOk6DqPUV8mcpyJkRz2x2R+/fYcWDYG3oWbG7/L7Yl/WqQ1VZCjnL9OTIMAn6c+BC5Eru4sQEw==} + + '@mapbox/unitbezier@0.0.0': + resolution: {integrity: sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA==} + + '@mapbox/vector-tile@1.3.1': + resolution: {integrity: sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==} + + '@mapbox/whoots-js@3.1.0': + resolution: {integrity: sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==} + engines: {node: '>=6.0.0'} + + '@napi-rs/canvas-android-arm64@0.1.100': + resolution: {integrity: sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@napi-rs/canvas-darwin-arm64@0.1.100': + resolution: {integrity: sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@napi-rs/canvas-darwin-x64@0.1.100': + resolution: {integrity: sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@napi-rs/canvas-linux-arm-gnueabihf@0.1.100': + resolution: {integrity: sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@napi-rs/canvas-linux-arm64-gnu@0.1.100': + resolution: {integrity: sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@napi-rs/canvas-linux-arm64-musl@0.1.100': + resolution: {integrity: sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@napi-rs/canvas-linux-riscv64-gnu@0.1.100': + resolution: {integrity: sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + + '@napi-rs/canvas-linux-x64-gnu@0.1.100': + resolution: {integrity: sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@napi-rs/canvas-linux-x64-musl@0.1.100': + resolution: {integrity: sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@napi-rs/canvas-win32-arm64-msvc@0.1.100': + resolution: {integrity: sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@napi-rs/canvas-win32-x64-msvc@0.1.100': + resolution: {integrity: sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@napi-rs/canvas@0.1.100': + resolution: {integrity: sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==} + engines: {node: '>= 10'} + '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} @@ -1159,10 +1327,6 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@nolyfill/is-core-module@1.0.39': - resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} - engines: {node: '>=12.4.0'} - '@oxc-parser/binding-android-arm-eabi@0.121.0': resolution: {integrity: sha512-n07FQcySwOlzap424/PLMtOkbS7xOu8nsJduKL8P3COGHKgKoDYXwoAHCbChfgFpHnviehrLWIPX0lKGtbEk/A==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1210,56 +1374,48 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [glibc] '@oxc-parser/binding-linux-arm64-musl@0.121.0': resolution: {integrity: sha512-qT663J/W8yQFw3dtscbEi9LKJevr20V7uWs2MPGTnvNZ3rm8anhhE16gXGpxDOHeg9raySaSHKhd4IGa3YZvuw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [musl] '@oxc-parser/binding-linux-ppc64-gnu@0.121.0': resolution: {integrity: sha512-mYNe4NhVvDBbPkAP8JaVS8lC1dsoJZWH5WCjpw5E+sjhk1R08wt3NnXYUzum7tIiWPfgQxbCMcoxgeemFASbRw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@oxc-parser/binding-linux-riscv64-gnu@0.121.0': resolution: {integrity: sha512-+QiFoGxhAbaI/amqX567784cDyyuZIpinBrJNxUzb+/L2aBRX67mN6Jv40pqduHf15yYByI+K5gUEygCuv0z9w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - libc: [glibc] '@oxc-parser/binding-linux-riscv64-musl@0.121.0': resolution: {integrity: sha512-9ykEgyTa5JD/Uhv2sttbKnCfl2PieUfOjyxJC/oDL2UO0qtXOtjPLl7H8Kaj5G7p3hIvFgu3YWvAxvE0sqY+hQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - libc: [musl] '@oxc-parser/binding-linux-s390x-gnu@0.121.0': resolution: {integrity: sha512-DB1EW5VHZdc1lIRjOI3bW/wV6R6y0xlfvdVrqj6kKi7Ayu2U3UqUBdq9KviVkcUGd5Oq+dROqvUEEFRXGAM7EQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - libc: [glibc] '@oxc-parser/binding-linux-x64-gnu@0.121.0': resolution: {integrity: sha512-s4lfobX9p4kPTclvMiH3gcQUd88VlnkMTF6n2MTMDAyX5FPNRhhRSFZK05Ykhf8Zy5NibV4PbGR6DnK7FGNN6A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [glibc] '@oxc-parser/binding-linux-x64-musl@0.121.0': resolution: {integrity: sha512-P9KlyTpuBuMi3NRGpJO8MicuGZfOoqZVRP1WjOecwx8yk4L/+mrCRNc5egSi0byhuReblBF2oVoDSMgV9Bj4Hw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [musl] '@oxc-parser/binding-openharmony-arm64@0.121.0': resolution: {integrity: sha512-R+4jrWOfF2OAPPhj3Eb3U5CaKNAH9/btMveMULIrcNW/hjfysFQlF8wE0GaVBr81dWz8JLgQlsxwctoL78JwXw==} @@ -1335,49 +1491,41 @@ packages: resolution: {integrity: sha512-heV2+jmXyYnUrpUXSPugqWDRpnsQcDm2AX4wzTuvgdlZfoNYO0O3W2AVpJYaDn9AG4JdM6Kxom8+foE7/BcSig==} cpu: [arm64] os: [linux] - libc: [glibc] '@oxc-resolver/binding-linux-arm64-musl@11.19.1': resolution: {integrity: sha512-jvo2Pjs1c9KPxMuMPIeQsgu0mOJF9rEb3y3TdpsrqwxRM+AN6/nDDwv45n5ZrUnQMsdBy5gIabioMKnQfWo9ew==} cpu: [arm64] os: [linux] - libc: [musl] '@oxc-resolver/binding-linux-ppc64-gnu@11.19.1': resolution: {integrity: sha512-vLmdNxWCdN7Uo5suays6A/+ywBby2PWBBPXctWPg5V0+eVuzsJxgAn6MMB4mPlshskYbppjpN2Zg83ArHze9gQ==} cpu: [ppc64] os: [linux] - libc: [glibc] '@oxc-resolver/binding-linux-riscv64-gnu@11.19.1': resolution: {integrity: sha512-/b+WgR+VTSBxzgOhDO7TlMXC1ufPIMR6Vj1zN+/x+MnyXGW7prTLzU9eW85Aj7Th7CCEG9ArCbTeqxCzFWdg2w==} cpu: [riscv64] os: [linux] - libc: [glibc] '@oxc-resolver/binding-linux-riscv64-musl@11.19.1': resolution: {integrity: sha512-YlRdeWb9j42p29ROh+h4eg/OQ3dTJlpHSa+84pUM9+p6i3djtPz1q55yLJhgW9XfDch7FN1pQ/Vd6YP+xfRIuw==} cpu: [riscv64] os: [linux] - libc: [musl] '@oxc-resolver/binding-linux-s390x-gnu@11.19.1': resolution: {integrity: sha512-EDpafVOQWF8/MJynsjOGFThcqhRHy417sRyLfQmeiamJ8qVhSKAn2Dn2VVKUGCjVB9C46VGjhNo7nOPUi1x6uA==} cpu: [s390x] os: [linux] - libc: [glibc] '@oxc-resolver/binding-linux-x64-gnu@11.19.1': resolution: {integrity: sha512-NxjZe+rqWhr+RT8/Ik+5ptA3oz7tUw361Wa5RWQXKnfqwSSHdHyrw6IdcTfYuml9dM856AlKWZIUXDmA9kkiBQ==} cpu: [x64] os: [linux] - libc: [glibc] '@oxc-resolver/binding-linux-x64-musl@11.19.1': resolution: {integrity: sha512-cM/hQwsO3ReJg5kR+SpI69DMfvNCp+A/eVR4b4YClE5bVZwz8rh2Nh05InhwI5HR/9cArbEkzMjcKgTHS6UaNw==} cpu: [x64] os: [linux] - libc: [musl] '@oxc-resolver/binding-openharmony-arm64@11.19.1': resolution: {integrity: sha512-QF080IowFB0+9Rh6RcD19bdgh49BpQHUW5TajG1qvWHvmrQznTZZjYlgE2ltLXyKY+qs4F/v5xuX1XS7Is+3qA==} @@ -1404,6 +1552,28 @@ packages: cpu: [x64] os: [win32] + '@poppinss/cliui@6.8.1': + resolution: {integrity: sha512-o/ssbwr+r6woG65rk9eFHnn9dVUphZr/Rk+4+05ENVMBWYpYhTJGdE9RobTG5JLFubvO4gWIyFeNlC+I4EM6eA==} + + '@poppinss/colors@4.1.6': + resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} + + '@poppinss/validator-lite@2.1.2': + resolution: {integrity: sha512-UhSG1ouT6r67VbEFHK/8ax3EMZYHioew9PqGmEZjV41G15aPZi6cyhXtBVvF9xqkHMflA5V680k7bQzV0kfD5w==} + + '@quansync/fs@1.0.0': + resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} + + '@redocly/ajv@8.11.2': + resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==} + + '@redocly/config@0.22.0': + resolution: {integrity: sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ==} + + '@redocly/openapi-core@1.34.14': + resolution: {integrity: sha512-y+xFx+Zz54Xhr8jUdnLENYnt7Y7GEDL6Q03ga7rTtX8DVwefX9H+hQEPgJp1nda7vdH+wJ9/HBVvyfBuW9x6rA==} + engines: {node: '>=18.17.0', npm: '>=9.5.0'} + '@repeaterjs/repeater@3.0.6': resolution: {integrity: sha512-Javneu5lsuhwNCryN+pXH93VPQ8g0dBX7wItHFgYiwQmzE1sVdg5tWHiOgHywzL2W21XQopa7IwIEnNbmeUJYA==} @@ -1442,42 +1612,36 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [musl] '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - libc: [glibc] '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [musl] '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==} @@ -1525,45 +1689,187 @@ packages: '@rolldown/pluginutils@1.0.0-rc.7': resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} + '@rollup/pluginutils@5.3.0': + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + '@sentry-internal/browser-utils@10.49.0': + resolution: {integrity: sha512-n0QRx0Ysx6mPfIydTkz7VP0FmwM+/EqMZiRqdsU3aTYsngE9GmEDV0OL1bAy6a8N/C1xf9vntkuAtj6N/8Z51w==} + engines: {node: '>=18'} + + '@sentry-internal/feedback@10.49.0': + resolution: {integrity: sha512-JNsUBGv0faCFE7MeZUH99Y9lU9qq3LBALbLxpE1x7ngNrQnVYRlcFgdqaD/btNBKr8awjYL8gmcSkHBWskGqLQ==} + engines: {node: '>=18'} + + '@sentry-internal/replay-canvas@10.49.0': + resolution: {integrity: sha512-7D/NrgH1Qwx5trDYaaTSSJmCb1yVQQLqFG4G/S9x2ltzl9876lSGJL8UeW8ReNQgF3CDAcwbmm/9aXaVSBUNZA==} + engines: {node: '>=18'} + + '@sentry-internal/replay@10.49.0': + resolution: {integrity: sha512-IEy4lwHVMiRE3JAcn+kFKjsTgalDOCSTf20SoFd+nkt6rN/k1RDyr4xpdfF//Kj3UdeTmbuibYjK5H/FLhhnGg==} + engines: {node: '>=18'} + + '@sentry/browser@10.49.0': + resolution: {integrity: sha512-bGCHc+wK2Dx67YoSbmtlt04alqWfQ+dasD/GVipVOq50gvw/BBIDHTEWRJEjACl+LrvszeY54V+24p8z4IgysA==} + engines: {node: '>=18'} + + '@sentry/core@10.49.0': + resolution: {integrity: sha512-UaFeum3LUM1mB0d67jvKnqId1yWQjyqmaDV6kWngG03x+jqXb08tJdGpSoxjXZe13jFBbiBL/wKDDYIK7rCK4g==} + engines: {node: '>=18'} + + '@sentry/react@10.49.0': + resolution: {integrity: sha512-WdfJve0orTiumr25Ozgs2p2KaJR9xV82Z5V9IYBi0TadsurSWK6xI6SAFjw84tQht9Fp8q4UCn3QYCnApF4BfA==} + engines: {node: '>=18'} + peerDependencies: + react: ^16.14.0 || 17.x || 18.x || 19.x + + '@simple-git/args-pathspec@1.0.3': + resolution: {integrity: sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA==} + + '@simple-git/argv-parser@1.1.1': + resolution: {integrity: sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw==} + '@sindresorhus/merge-streams@4.0.0': resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} - '@stylistic/eslint-plugin-js@1.8.1': - resolution: {integrity: sha512-c5c2C8Mos5tTQd+NWpqwEu7VT6SSRooAguFPMj1cp2RkTYl1ynKoXo8MWy3k4rkbzoeYHrqC2UlUzsroAN7wtQ==} - engines: {node: ^16.0.0 || >=18.0.0} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@stylistic/stylelint-plugin@5.1.0': + resolution: {integrity: sha512-TFvKCbJUEWUYCD+rDv45qhnStO6nRtbBngaCblS2JGh8c95S3jJi3fIotfF6EDo4IVM15UPa65WP+kp6GNvXRA==} + engines: {node: '>=20.19.0'} + peerDependencies: + stylelint: ^17.6.0 + + '@svgr/babel-plugin-add-jsx-attribute@8.0.0': + resolution: {integrity: sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==} + engines: {node: '>=14'} peerDependencies: - eslint: '>=8.40.0' + '@babel/core': ^7.0.0-0 - '@stylistic/eslint-plugin-jsx@1.8.1': - resolution: {integrity: sha512-k1Eb6rcjMP+mmjvj+vd9y5KUdWn1OBkkPLHXhsrHt5lCDFZxJEs0aVQzE5lpYrtVZVkpc5esTtss/cPJux0lfA==} - engines: {node: ^16.0.0 || >=18.0.0} + '@svgr/babel-plugin-remove-jsx-attribute@8.0.0': + resolution: {integrity: sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==} + engines: {node: '>=14'} peerDependencies: - eslint: '>=8.40.0' + '@babel/core': ^7.0.0-0 - '@stylistic/eslint-plugin-plus@1.8.1': - resolution: {integrity: sha512-4+40H3lHYTN8OWz+US8CamVkO+2hxNLp9+CAjorI7top/lHqemhpJvKA1LD9Uh+WMY9DYWiWpL2+SZ2wAXY9fQ==} + '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0': + resolution: {integrity: sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==} + engines: {node: '>=14'} peerDependencies: - eslint: '*' + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0': + resolution: {integrity: sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-svg-dynamic-title@8.0.0': + resolution: {integrity: sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-svg-em-dimensions@8.0.0': + resolution: {integrity: sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-transform-react-native-svg@8.1.0': + resolution: {integrity: sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-transform-svg-component@8.0.0': + resolution: {integrity: sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==} + engines: {node: '>=12'} + peerDependencies: + '@babel/core': ^7.0.0-0 - '@stylistic/eslint-plugin-ts@1.8.1': - resolution: {integrity: sha512-/q1m+ZuO1JHfiSF16EATFzv7XSJkc5W6DocfvH5o9oB6WWYFMF77fVoBWnKT3wGptPOc2hkRupRKhmeFROdfWA==} - engines: {node: ^16.0.0 || >=18.0.0} + '@svgr/babel-preset@8.1.0': + resolution: {integrity: sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==} + engines: {node: '>=14'} peerDependencies: - eslint: '>=8.40.0' + '@babel/core': ^7.0.0-0 + + '@svgr/core@8.1.0': + resolution: {integrity: sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==} + engines: {node: '>=14'} + + '@svgr/hast-util-to-babel-ast@8.0.0': + resolution: {integrity: sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==} + engines: {node: '>=14'} - '@stylistic/eslint-plugin@1.8.1': - resolution: {integrity: sha512-64My6I7uCcmSQ//427Pfg2vjSf9SDzfsGIWohNFgISMLYdC5BzJqDo647iDDJzSxINh3WTC0Ql46ifiKuOoTyA==} - engines: {node: ^16.0.0 || >=18.0.0} + '@svgr/plugin-jsx@8.1.0': + resolution: {integrity: sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==} + engines: {node: '>=14'} peerDependencies: - eslint: '>=8.40.0' + '@svgr/core': '*' '@togglecorp/fujs@2.2.0': resolution: {integrity: sha512-OuoQ9Bj7SiI2sTLpaM/HivU6HpSbZ3ANBIn7f9KUz5eFcfwBBEDvjI+4ah6WktJEYTUKY4RxX37z64qOrTJSwA==} + '@togglecorp/re-map@0.3.0': + resolution: {integrity: sha512-tCohiZxUt5tkuAmzTz8qZu2foliPkj1ZyNnS3Z4He9ZDKOuGrZy0mwRe8QDDDTF+lIULD4gQMaQIENaWRls/Uw==} + peerDependencies: + mapbox-gl: ^1.13.0 + react: ^17.0.2 + react-dom: ^17.0.2 + + '@togglecorp/toggle-form@2.0.4': + resolution: {integrity: sha512-+EzRzXK/PKlisu44yARpxOkoeowz+0oKk2Rl3CdhxtBfTVfzG28aHAklDTubTBssS8hneGBTav2aInCqmwChfg==} + peerDependencies: + react: ^18.2.0 + react-dom: ^18.2.0 + + '@togglecorp/toggle-request@1.0.0-beta.3': + resolution: {integrity: sha512-q36QiIGkmWtJUrgrpLvPbwIx9MrRZI/dNqI0P16oLQuboc2ZBvWjBWnfHgIlzWDj3gGgBfAzaSzS+DTlNF3tQw==} + peerDependencies: + react: ^17.0.2 + react-dom: ^17.0.2 + + '@togglecorp/vite-plugin-validate-env@2.2.1': + resolution: {integrity: sha512-K39bpXSOdliJPMNfHuK17pszgWWNRJfaB4NRDQBuWx9h4O4/qF1nl7DpwBqSB006e3E284jZAZEw2r7RoHdG4w==} + engines: {node: '>=22'} + peerDependencies: + vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + '@turf/bbox@7.3.5': + resolution: {integrity: sha512-oG1ya/HtBjAIg4TimbWx+nOYPbY0bCvt82Bq8tm6sBw3qqtbOyRSfDz79Sq90TnH7DXJprJ1qnVGKNtZ6jemfw==} + + '@turf/buffer@7.3.5': + resolution: {integrity: sha512-TGls3nYtWzviKHT00XVBfHKa7Z2oIZKqiHN7R0xErGwMSAR7YhxVROhxq/iyIsWZjl1SlPwweZZIxWILQuxpZA==} + + '@turf/center@7.3.5': + resolution: {integrity: sha512-eub5/Kfdmn89ZqwCONHI7astmTDEtN5M6+JfOkgoSyhKKFhUJYNxUyH1F/vCtIP7j1K369Vs4L9TYiuGapvIKQ==} + + '@turf/clone@7.3.5': + resolution: {integrity: sha512-qfIaHj3410QEcTpiCRnTzhq8YrUp2gWrUIPLBAEFykopNxJkq1du1VrRzvuAo36ap2UV7KppkS6wGNypbcxswQ==} + + '@turf/helpers@7.3.5': + resolution: {integrity: sha512-E/NMGV5MwbjjP7AJXBtsanC3yY8N2MQ87IGdIgkB2ji5AtBpwnH4L3gEqpYN4RlCJJWbLbzO91BbKv2waUd0eg==} + + '@turf/jsts@2.7.2': + resolution: {integrity: sha512-zAezGlwWHPyU0zxwcX2wQY3RkRpwuoBmhhNE9HY9kWhFDkCxZ3aWK5URKwa/SWKJbj9aztO+8vtdiBA28KVJFg==} + + '@turf/meta@7.3.5': + resolution: {integrity: sha512-r+ohqxoyqeigFB0oFrQx/YEHIkOKqcKpCjvZkvZs7Tkv+IFco5MezAd2zd4rzK+0DfFgDP3KpJc7HqrYjvEjhg==} + + '@turf/projection@7.3.5': + resolution: {integrity: sha512-G4bejYKT0vCQZryMhEoS9aLmP7ThDg6nb3zi3wPzELiTrGNOd2YgkWVheQDGCk4hcqEIWZc9fI2alaRSSkkLVw==} + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -1579,12 +1885,15 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} - '@types/eslint@8.56.12': - resolution: {integrity: sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g==} - '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/file-saver@2.0.7': + resolution: {integrity: sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==} + + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + '@types/hoist-non-react-statics@3.3.7': resolution: {integrity: sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==} peerDependencies: @@ -1596,9 +1905,15 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/mapbox-gl@1.13.10': + resolution: {integrity: sha512-0oUy5d5nT3L480MRviAnaBUEXuWCG/7M4ZQo0n8eJ/LLMgJ0nMbjv7M+qoPl4TAj6yVVWKTvkukXvW9QHH1GVw==} + '@types/node@24.12.2': resolution: {integrity: sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==} + '@types/papaparse@5.5.2': + resolution: {integrity: sha512-gFnFp/JMzLHCwRf7tQHrNnfhN4eYBVYYI897CGX4MY1tzY9l2aLkVyx2IlKZ/SAqDbB3I1AOZW5gTMGGsqWliA==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -1607,23 +1922,9 @@ packages: '@types/react@19.2.14': resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} - '@types/semver@7.7.1': - resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} - '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - '@typescript-eslint/eslint-plugin@6.21.0': - resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha - eslint: ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - '@typescript-eslint/eslint-plugin@8.58.0': resolution: {integrity: sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1632,16 +1933,6 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@6.21.0': - resolution: {integrity: sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - '@typescript-eslint/parser@8.58.0': resolution: {integrity: sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1655,10 +1946,6 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@6.21.0': - resolution: {integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==} - engines: {node: ^16.0.0 || >=18.0.0} - '@typescript-eslint/scope-manager@8.58.0': resolution: {integrity: sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1669,16 +1956,6 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@6.21.0': - resolution: {integrity: sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - '@typescript-eslint/type-utils@8.58.0': resolution: {integrity: sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1686,35 +1963,16 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@6.21.0': - resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==} - engines: {node: ^16.0.0 || >=18.0.0} - '@typescript-eslint/types@8.58.0': resolution: {integrity: sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@6.21.0': - resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - '@typescript-eslint/typescript-estree@8.58.0': resolution: {integrity: sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@6.21.0': - resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - '@typescript-eslint/utils@8.58.0': resolution: {integrity: sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1722,17 +1980,10 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@6.21.0': - resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==} - engines: {node: ^16.0.0 || >=18.0.0} - '@typescript-eslint/visitor-keys@8.58.0': resolution: {integrity: sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@ungap/structured-clone@1.3.0': - resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - '@unrs/resolver-binding-android-arm-eabi@1.11.1': resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} cpu: [arm] @@ -1772,49 +2023,41 @@ packages: resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} cpu: [arm64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] - libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} @@ -1904,16 +2147,36 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + ajv@6.14.0: resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} ajv@8.18.0: resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-escapes@3.2.0: + resolution: {integrity: sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==} + engines: {node: '>=4'} + ansi-escapes@7.3.0: resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} engines: {node: '>=18'} + ansi-regex@3.0.1: + resolution: {integrity: sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==} + engines: {node: '>=4'} + + ansi-regex@4.1.1: + resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} + engines: {node: '>=6'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -1922,6 +2185,10 @@ packages: resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} engines: {node: '>=12'} + ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -1930,6 +2197,9 @@ packages: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -1980,6 +2250,10 @@ packages: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} + arrify@1.0.1: + resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} + engines: {node: '>=0.10.0'} + asn1@0.2.6: resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} @@ -2051,8 +2325,8 @@ packages: brace-expansion@1.1.13: resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==} - brace-expansion@2.0.3: - resolution: {integrity: sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==} + brace-expansion@2.1.0: + resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==} brace-expansion@5.0.5: resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} @@ -2070,13 +2344,6 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - builtin-modules@3.3.0: - resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} - engines: {node: '>=6'} - - builtins@5.1.0: - resolution: {integrity: sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==} - cacheable@2.3.4: resolution: {integrity: sha512-djgxybDbw9fL/ZWMI3+CE8ZilNxcwFkVtDc1gJ+IlOSSWkSMPQabhV/XCHTQ6pwwN6aivXPZ43omTooZiX06Ew==} @@ -2099,6 +2366,14 @@ packages: camel-case@4.1.2: resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + caniuse-lite@1.0.30001786: resolution: {integrity: sha512-4oxTZEvqmLLrERwxO76yfKM7acZo310U+v4kqexI2TL1DkkUEMT8UijrxxcnVdxR3qkVf5awGRX+4Z6aPHVKrA==} @@ -2108,6 +2383,10 @@ packages: caseless@0.12.0: resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} + chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -2118,28 +2397,78 @@ packages: change-case@4.1.2: resolution: {integrity: sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==} + change-case@5.4.4: + resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} + + chardet@0.7.0: + resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + chardet@2.1.1: resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + cli-boxes@4.0.1: + resolution: {integrity: sha512-5IOn+jcCEHEraYolBPs/sT4BxYCe2nHg374OPiItB1O96KZFseS2gthU4twyYzeDcFew4DaUM/xwc5BQf08JJw==} + engines: {node: '>=18.20 <19 || >=20.10'} + + cli-cursor@2.1.0: + resolution: {integrity: sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==} + engines: {node: '>=4'} + cli-cursor@5.0.0: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cli-table3@0.6.5: + resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} + engines: {node: 10.* || >= 12.*} + + cli-table@0.3.11: + resolution: {integrity: sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ==} + engines: {node: '>= 0.2.0'} + cli-truncate@5.2.0: resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==} engines: {node: '>=20'} + cli-width@2.2.1: + resolution: {integrity: sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==} + cli-width@4.1.0: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} + cliui@5.0.0: + resolution: {integrity: sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} @@ -2147,13 +2476,23 @@ packages: colord@2.9.3: resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} + colorette@1.4.0: + resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==} + colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + colors@1.0.3: + resolution: {integrity: sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==} + engines: {node: '>=0.1.90'} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + common-tags@1.8.2: resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} engines: {node: '>=4.0.0'} @@ -2161,9 +2500,6 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - confbox@0.1.8: - resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} - confusing-browser-globals@1.0.11: resolution: {integrity: sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==} @@ -2235,6 +2571,9 @@ packages: resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + csscolorparser@1.0.3: + resolution: {integrity: sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==} + cssdb@8.8.0: resolution: {integrity: sha512-QbLeyz2Bgso1iRlh7IpWk6OKa3lLNGXsujVjDMPl9rOZpxKeiG69icLpbLCFxeURwmcdIfZqQyhlooKJYM4f8Q==} @@ -2256,6 +2595,12 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + d3-array@1.2.4: + resolution: {integrity: sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==} + + d3-geo@1.7.1: + resolution: {integrity: sha512-O4AempWAr+P5qbk2bC2FuN/sDW4z+dN2wDf9QV3bxQt4M5HfOEeXLgJ/UKQW0+o1Dj8BE+L5kiDbdWUMjsmQpw==} + damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} @@ -2306,6 +2651,10 @@ packages: supports-color: optional: true + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -2313,6 +2662,9 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -2321,6 +2673,9 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -2329,6 +2684,10 @@ packages: resolution: {integrity: sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==} engines: {node: '>=4'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} @@ -2348,10 +2707,6 @@ packages: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} - doctrine@3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} - dom-serializer@2.0.0: resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} @@ -2392,6 +2747,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + earcut@2.2.4: + resolution: {integrity: sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==} + ecc-jsbn@0.1.2: resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==} @@ -2401,6 +2759,9 @@ packages: emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + emoji-regex@7.0.3: + resolution: {integrity: sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -2458,10 +2819,17 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} + es6-promise@3.3.1: + resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==} + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -2471,12 +2839,6 @@ packages: engines: {node: '>=4.0'} hasBin: true - eslint-compat-utils@0.5.1: - resolution: {integrity: sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==} - engines: {node: '>=12'} - peerDependencies: - eslint: '>=6.0.0' - eslint-config-airbnb-base@15.0.0: resolution: {integrity: sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==} engines: {node: ^10.12.0 || >=12.0.0} @@ -2484,10 +2846,6 @@ packages: eslint: ^7.32.0 || ^8.2.0 eslint-plugin-import: ^2.25.2 - eslint-config-airbnb-flat@0.0.12: - resolution: {integrity: sha512-NTT3Ly9GhddqgD+jr9zkCD8LbsKkuwXGAlPbpOy3gLx147lgRiMvteKujKVjxM2rZJv3Fw1o9jZO1sTxGZyPUg==} - engines: {node: '>=18'} - eslint-config-airbnb@19.0.4: resolution: {integrity: sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==} engines: {node: ^10.12.0 || ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -2510,19 +2868,6 @@ packages: eslint-import-resolver-node@0.3.10: resolution: {integrity: sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==} - eslint-import-resolver-typescript@3.10.1: - resolution: {integrity: sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==} - engines: {node: ^14.18.0 || >=16.0.0} - peerDependencies: - eslint: '*' - eslint-plugin-import: '*' - eslint-plugin-import-x: '*' - peerDependenciesMeta: - eslint-plugin-import: - optional: true - eslint-plugin-import-x: - optional: true - eslint-import-resolver-typescript@4.4.4: resolution: {integrity: sha512-1iM2zeBvrYmUNTj2vSC/90JTHDth+dfOfiNKkxApWRsTJYNrc8rOdxxIf5vazX+BiAXTeOT0UvWpGI/7qIWQOw==} engines: {node: ^16.17.0 || >=18.6.0} @@ -2557,19 +2902,6 @@ packages: eslint-import-resolver-webpack: optional: true - eslint-plugin-es-x@7.8.0: - resolution: {integrity: sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ==} - engines: {node: ^14.18.0 || >=16.0.0} - peerDependencies: - eslint: '>=8' - - eslint-plugin-i@2.29.1: - resolution: {integrity: sha512-ORizX37MelIWLbMyqI7hi8VJMf7A0CskMmYkB+lkCX3aF4pkGV7kwx5bSEb4qx7Yce2rAf9s34HqDRPjGRZPNQ==} - engines: {node: '>=12'} - deprecated: Please migrate to the brand new `eslint-plugin-import-x` instead - peerDependencies: - eslint: ^7.2.0 || ^8 - eslint-plugin-import-exports-imports-resolver@1.0.1: resolution: {integrity: sha512-4Gqp25iQSS3k8o0/zKxymWbnDW8KIqkubrOOy67IU9Qmhmkq4AiuMXbjx9O9AhYG7Vl94ZQFBcpfwLaQkINv2w==} @@ -2596,28 +2928,16 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 - eslint-plugin-n@16.6.2: - resolution: {integrity: sha512-6TyDmZ1HXoFQXnhCTUjVFULReoBPOAjpuiKELMkeP40yffI/1ZRO+d9ug/VC6fqISo2WkuIBk3cvuRPALaWlOQ==} - engines: {node: '>=16.0.0'} - peerDependencies: - eslint: '>=7.0.0' - - eslint-plugin-react-compiler@19.1.0-rc.2: - resolution: {integrity: sha512-oKalwDGcD+RX9mf3NEO4zOoUMeLvjSvcbbEOpquzmzqEEM2MQdp7/FY/Hx9NzmUwFzH1W9SKTz5fihfMldpEYw==} - engines: {node: ^14.17.0 || ^16.0.0 || >= 18.0.0} - peerDependencies: - eslint: '>=7' - eslint-plugin-react-hooks@7.0.1: resolution: {integrity: sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==} engines: {node: '>=18'} peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 - eslint-plugin-react-refresh@0.5.2: - resolution: {integrity: sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==} + eslint-plugin-react-refresh@0.4.26: + resolution: {integrity: sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==} peerDependencies: - eslint: ^9 || ^10 + eslint: '>=8.40' eslint-plugin-react@7.37.5: resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} @@ -2630,10 +2950,6 @@ packages: peerDependencies: eslint: '>=5.0.0' - eslint-scope@7.2.2: - resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - eslint-scope@8.4.0: resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2650,12 +2966,6 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@8.57.1: - resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. - hasBin: true - eslint@9.39.4: resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2670,10 +2980,6 @@ packages: resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - espree@9.6.1: - resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} @@ -2695,6 +3001,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -2702,9 +3011,17 @@ packages: eventemitter3@5.0.4: resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + external-editor@3.1.0: + resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} + engines: {node: '>=4'} + extsprintf@1.3.0: resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==} engines: {'0': node >=0.6.0} @@ -2748,29 +3065,32 @@ packages: resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} engines: {node: ^12.20 || >= 14.13} + figures@2.0.0: + resolution: {integrity: sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==} + engines: {node: '>=4'} + file-entry-cache@11.1.2: resolution: {integrity: sha512-N2WFfK12gmrK1c1GXOqiAJ1tc5YE+R53zvQ+t5P8S5XhnmKYVB5eZEiLNZKDSmoG8wqqbF9EXYBBW/nef19log==} - file-entry-cache@6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} - file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} + file-saver@2.0.5: + resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + find-up@3.0.0: + resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} + engines: {node: '>=6'} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} - flat-cache@3.2.0: - resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} - engines: {node: ^10.12.0 || >=12.0.0} - flat-cache@4.0.1: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} @@ -2808,6 +3128,10 @@ packages: fraction.js@5.3.4: resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + fs-exists-sync@0.1.0: + resolution: {integrity: sha512-cR/vflFyPZtrN6b38ZyWxpWdhlXrzZEBawlpBQMq7033xVY7/kg0GDMBK5jg8lDYQckdJ5x/YC88lM3C7VMsLg==} + engines: {node: '>=0.10.0'} + fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -2834,6 +3158,9 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + geojson-vt@3.2.1: + resolution: {integrity: sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==} + get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -2854,6 +3181,10 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + get-symbol-description@1.1.0: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} @@ -2864,6 +3195,9 @@ packages: getpass@0.1.7: resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==} + gl-matrix@3.4.4: + resolution: {integrity: sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -2884,10 +3218,6 @@ packages: resolution: {integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==} engines: {node: '>=6'} - globals@13.24.0: - resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} - engines: {node: '>=8'} - globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -2911,15 +3241,12 @@ packages: globjoin@0.1.4: resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==} - globrex@0.1.2: - resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} - gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} - graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} graphql-config@5.1.6: resolution: {integrity: sha512-fCkYnm4Kdq3un0YIM4BCZHVR5xl0UeLP6syxxO7KAstdY7QVyVvTHP0kRPDYEP1v08uwtJVgis5sj3IOTLOniQ==} @@ -2931,6 +3258,12 @@ packages: cosmiconfig-toml-loader: optional: true + graphql-depth-limit@1.1.0: + resolution: {integrity: sha512-+3B2BaG8qQ8E18kzk9yiSdAa75i/hnnOwgSeAxVJctGQPvmeiLtqKOYF6HETCyRjiF7Xfsyal0HbLlxCQkgkrw==} + engines: {node: '>=6.0.0'} + peerDependencies: + graphql: '*' + graphql-tag@2.12.6: resolution: {integrity: sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==} engines: {node: '>=10'} @@ -2957,6 +3290,9 @@ packages: resolution: {integrity: sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + grid-index@1.1.0: + resolution: {integrity: sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==} + har-schema@2.0.0: resolution: {integrity: sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==} engines: {node: '>=4'} @@ -2970,6 +3306,10 @@ packages: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -3029,10 +3369,17 @@ packages: htmlparser2@10.1.0: resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} + http-post-message@0.3.0: + resolution: {integrity: sha512-tfn1ca5RcRKdejbFanY2ydN4pXB+youdgtNz1GSKtWvzbaf63sGQWyRjo5bnqlebTjG85wY0GUADDD0tAVpmkQ==} + http-signature@1.2.0: resolution: {integrity: sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==} engines: {node: '>=0.8', npm: '>=1.3.7'} + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -3041,6 +3388,9 @@ packages: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -3067,6 +3417,10 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + index-to-position@1.2.0: + resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==} + engines: {node: '>=18'} + inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. @@ -3077,6 +3431,10 @@ packages: ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + inquirer@6.5.2: + resolution: {integrity: sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==} + engines: {node: '>=6.0.0'} + internal-slot@1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} @@ -3111,10 +3469,6 @@ packages: resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} engines: {node: '>= 0.4'} - is-builtin-module@3.2.1: - resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} - engines: {node: '>=6'} - is-bun-module@2.0.0: resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} @@ -3134,6 +3488,10 @@ packages: resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} engines: {node: '>= 0.4'} + is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -3142,6 +3500,10 @@ packages: resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} engines: {node: '>= 0.4'} + is-fullwidth-code-point@2.0.0: + resolution: {integrity: sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==} + engines: {node: '>=4'} + is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} @@ -3177,10 +3539,6 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} - is-path-inside@4.0.0: resolution: {integrity: sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==} engines: {node: '>=12'} @@ -3274,9 +3632,17 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + js-levenshtein@1.1.6: + resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} + engines: {node: '>=0.10.0'} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + hasBin: true + js-yaml@4.1.1: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true @@ -3336,10 +3702,17 @@ packages: resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==} engines: {node: '>=0.6.0'} + jsts@2.7.1: + resolution: {integrity: sha512-x2wSZHEBK20CY+Wy+BPE7MrFQHW6sIsdaGUMEqmGAio+3gFzQaBYPwLRonUfQf9Ak8pBieqj9tUofX1+WtAEIg==} + engines: {node: '>= 12'} + jsx-ast-utils@3.3.5: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} + kdbush@3.0.0: + resolution: {integrity: sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -3350,6 +3723,10 @@ packages: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + knip@6.3.0: resolution: {integrity: sha512-g6dVPoTw6iNm3cubC5IWxVkVsd0r5hXhTBTbAGIEQN53GdA2ZM/slMTPJ7n5l8pBebNQPHpxjmKxuR4xVQ2/hQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3405,28 +3782,24 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} @@ -3447,18 +3820,33 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + lint@0.8.19: + resolution: {integrity: sha512-i9iqBX/OO2+zSE7hEDXJ0rdLMxvBluK2T/xbCKAhEgyHE1q6kjp1HJGOVagkVB0f0UZ+FnW/wM3smsihQN0tFw==} + hasBin: true + listr2@9.0.5: resolution: {integrity: sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==} engines: {node: '>=20.0.0'} - local-pkg@0.5.1: - resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==} - engines: {node: '>=14'} + loadash@1.0.0: + resolution: {integrity: sha512-xlX5HBsXB3KG0FJbJJG/3kYWCfsCyCSus3T+uHVu6QL6YxAdggmm3QeyLgn54N2yi5/UE6xxL5ZWJAAiHzHYEg==} + deprecated: Package is unsupport. Please use the lodash package instead. + + locate-path@3.0.0: + resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} + engines: {node: '>=6'} locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. + + lodash.lowercase@4.3.0: + resolution: {integrity: sha512-UcvP1IZYyDKyEL64mmrwoA1AbFu5ahojhTtkOUr1K9dbuxzS9ev8i4TxMMGCqRC9TE8uDaSoufNAXxRPNTseVA==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} @@ -3471,6 +3859,10 @@ packages: lodash@4.18.1: resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + log-symbols@2.2.0: + resolution: {integrity: sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==} + engines: {node: '>=4'} + log-symbols@4.1.0: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} @@ -3479,6 +3871,10 @@ packages: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} + log-update@7.2.0: + resolution: {integrity: sha512-iLs7dGSyjZiUgvrUvuD3FndAxVJk+TywBkkkwUSm9HdYoskJalWg5qVsEiXeufPvRVPbCUmNQewg798rx+sPXg==} + engines: {node: '>=20'} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -3492,10 +3888,20 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + make-cancellable-promise@2.0.0: + resolution: {integrity: sha512-3SEQqTpV9oqVsIWqAcmDuaNeo7yBO3tqPtqGRcKkEo0lrzD3wqbKG9mkxO65KoOgXqj+zH2phJ2LiAsdzlogSw==} + + make-event-props@2.0.0: + resolution: {integrity: sha512-G/hncXrl4Qt7mauJEXSg3AcdYzmpkIITTNl5I+rH9sog5Yw0kK6vseJjCaPfOXqOqQuPUP89Rkhfz5kPS8ijtw==} + map-cache@0.2.2: resolution: {integrity: sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==} engines: {node: '>=0.10.0'} + mapbox-gl@1.13.3: + resolution: {integrity: sha512-p8lJFEiqmEQlyv+DQxFAOG/XPWN0Wp7j/Psq93Zywz7qt9CcUKFYDBOoOEKzqe6gudHVJY8/Bhqw6VDpX2lSBg==} + engines: {node: '>=6.4.0'} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -3510,6 +3916,14 @@ packages: resolution: {integrity: sha512-EDYo6VlmtnumlcBCbh1gLJ//9jvM/ndXHfVXIFrZVr6fGcwTUyCTFNTLCKuY3ffbK8L/+3Mzqnd58RojiZqHVw==} engines: {node: '>=20'} + merge-refs@2.0.0: + resolution: {integrity: sha512-3+B21mYK2IqUWnd2EivABLT7ueDhb0b8/dGK8LoFQPrU61YITeCMn14F7y7qZafWNZhUEKb24cJdiT5Wxs3prg==} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -3535,6 +3949,10 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mimic-fn@1.2.0: + resolution: {integrity: sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==} + engines: {node: '>=4'} + mimic-function@5.0.1: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} @@ -3546,19 +3964,29 @@ packages: minimatch@3.1.5: resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} - minimatch@9.0.3: - resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} - engines: {node: '>=16 || 14 >=14.17'} + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - mlly@1.8.2: - resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + + moment@2.30.1: + resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + murmurhash-js@1.0.0: + resolution: {integrity: sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==} + + mute-stream@0.0.7: + resolution: {integrity: sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==} + mute-stream@2.0.0: resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} engines: {node: ^18.17.0 || >=20.5.0} @@ -3603,6 +4031,10 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + nwsapi@2.2.23: resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} @@ -3644,10 +4076,20 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + onetime@2.0.1: + resolution: {integrity: sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==} + engines: {node: '>=4'} + onetime@7.0.0: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} + openapi-typescript@7.13.0: + resolution: {integrity: sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ==} + hasBin: true + peerDependencies: + typescript: ^5.x + option-t@20.3.1: resolution: {integrity: sha512-umjR1qtje0FD7AJbPmrzaaYCmHkh9yWDWUfRtcN8P3o5pv/JYaAVsXu0t3sRj2/Ogcp6Q9jrGRKBWX5DyiQFMQ==} @@ -3659,6 +4101,14 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} + ora@3.4.0: + resolution: {integrity: sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==} + engines: {node: '>=6'} + + os-tmpdir@1.0.2: + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} + own-keys@1.0.1: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} @@ -3670,14 +4120,29 @@ packages: oxc-resolver@11.19.1: resolution: {integrity: sha512-qE/CIg/spwrTBFt5aKmwe3ifeDdLfA2NESN30E42X/lII5ClF8V7Wt6WIJhcGZjp0/Q+nQ+9vgxGk//xZNX2hg==} + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} + p-locate@3.0.0: + resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} + engines: {node: '>=6'} + p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + papaparse@5.5.3: + resolution: {integrity: sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==} + param-case@3.0.4: resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} @@ -3693,6 +4158,10 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + parse-json@8.3.0: + resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==} + engines: {node: '>=18'} + parse-srcset@1.0.2: resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==} @@ -3705,6 +4174,10 @@ packages: path-case@3.0.4: resolution: {integrity: sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==} + path-exists@3.0.0: + resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} + engines: {node: '>=4'} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -3717,6 +4190,10 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -3732,12 +4209,17 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} - pathe@2.0.3: - resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - pattern-key-compare@1.0.0: resolution: {integrity: sha512-7wi8a7OFmdx4Hx31+KY9kcD7gO+MWWupXtlAx7ANqoE8Pypl501FsDAPX2tSYLOuafED82A0Mv3lzeNfn82Jlg==} + pbf@3.3.0: + resolution: {integrity: sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==} + hasBin: true + + pdfjs-dist@5.4.296: + resolution: {integrity: sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==} + engines: {node: '>=20.16.0 || >=22.3.0'} + performance-now@2.1.0: resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} @@ -3755,8 +4237,9 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} - pkg-types@1.3.1: - resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} pn@1.1.0: resolution: {integrity: sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==} @@ -3970,6 +4453,23 @@ packages: resolution: {integrity: sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==} engines: {node: ^10 || ^12 || >=14} + potpack@1.0.2: + resolution: {integrity: sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==} + + powerbi-client-react@2.0.2: + resolution: {integrity: sha512-NXn2gAQlktKQTyMeOkH8dEUUN79MTiyq6HKWG72WyayDcvcIb9mTFtlYK9CyD8efUeWa4QsiYMOg2xrkAFd4JQ==} + peerDependencies: + react: '>= 18' + + powerbi-client@2.23.10: + resolution: {integrity: sha512-jqAatMokgk6c204R92ZDwbbCZKtskrZ3GPaUXo1TDUiD89jg22GP1xkCwf2tMnrbG1VyWGjQIyAg+/HZPtyKNg==} + + powerbi-models@2.0.1: + resolution: {integrity: sha512-hYYbxCNB3VJ/vn/mbldWHXLvJYHCS4ECzUEskzlTtaaVFILMhMr2r9p3wUjGTIUmZZQ6Ve8KkVT2o1w+aGjiGw==} + + powerbi-router@0.1.5: + resolution: {integrity: sha512-DFJCKxwh/DqMZXtHSo6xZl87mbRviZGn4P7Oi2rT0L4HMI4AjnWIrwg0JCSM7ymBzYnNe5UmrsCaf2Upur5RQA==} + prelude-ls@1.1.2: resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} engines: {node: '>= 0.8.0'} @@ -3978,9 +4478,24 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prettier@1.19.1: + resolution: {integrity: sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==} + engines: {node: '>=4'} + hasBin: true + + pretty-hrtime@1.0.3: + resolution: {integrity: sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==} + engines: {node: '>= 0.8'} + prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + + protocol-buffers-schema@3.6.1: + resolution: {integrity: sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==} + psl@1.15.0: resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} @@ -3996,9 +4511,15 @@ packages: resolution: {integrity: sha512-mzR4sElr1bfCaPJe7m8ilJ6ZXdDaGoObcYR0ZHSsktM/Lt21MVHj5De30GQH2eiZ1qGRTO7LCAzQsUeXTNexWQ==} engines: {node: '>=0.6'} + quansync@1.0.0: + resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + quickselect@2.0.0: + resolution: {integrity: sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==} + react-clientside-effect@1.2.8: resolution: {integrity: sha512-ma2FePH0z3px2+WOu6h+YycZcEvFmmxIlAb62cF52bG86eMySciO/EQZeQMXd07kPCYB0a1dWDT5J+KE9mCDUw==} peerDependencies: @@ -4036,6 +4557,16 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-pdf@10.4.1: + resolution: {integrity: sha512-kS/35staVCBqS29verTQJQZXw7RfsRCPO3fdJoW1KXylcv7A9dw6DZ3vJXC2w+bIBgLw5FN4pOFvKSQtkQhPfA==} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + react-remove-scroll-bar@2.3.8: resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} engines: {node: '>=10'} @@ -4080,6 +4611,10 @@ packages: resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -4097,6 +4632,10 @@ packages: remove-trailing-spaces@1.0.9: resolution: {integrity: sha512-xzG7w5IRijvIkHIjDk65URsJJ7k4J95wmcArY5PRcmjldIOl7oTvG8+X2Ag690R7SfwiOcHrWZKVc1Pp5WIOzA==} + replace-in-file@3.4.4: + resolution: {integrity: sha512-ehq0dFsxSpfPiPLBU5kli38Ud8bZL0CQKG8WQVbvhmyilXaMJ8y4LtDZs/K3MD8C0+rHbsfW8c9r2bUEy0B/6Q==} + hasBin: true + request-promise-core@1.1.4: resolution: {integrity: sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==} engines: {node: '>=0.10.0'} @@ -4123,6 +4662,9 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -4134,6 +4676,9 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve-protobuf-schema@2.1.0: + resolution: {integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==} + resolve.exports@1.1.1: resolution: {integrity: sha512-/NtpHNDN7jWhAaQ9BvBUYZ6YTXsRBgfqWFWP7BZBaoMJO/I3G5OFzvTuWNlZC3aPjins1F+TNrLKsGbH4rfsRQ==} engines: {node: '>=10'} @@ -4151,10 +4696,18 @@ packages: engines: {node: '>= 0.4'} hasBin: true + restore-cursor@2.0.0: + resolution: {integrity: sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==} + engines: {node: '>=4'} + restore-cursor@5.1.0: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -4162,19 +4715,28 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} - rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - deprecated: Rimraf versions prior to v4 are no longer supported - hasBin: true - rolldown@1.0.0-rc.12: resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + route-recognizer@0.1.11: + resolution: {integrity: sha512-7JNu5mXQVa39zxmUKyk/bfpeF2WyEC5JKVTJO5HATcoUQpcQsI3eLzhwGU69xeOagQxfOQ+yr2sSv0G8xy+vQA==} + + run-async@2.4.1: + resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} + engines: {node: '>=0.12.0'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + + rxjs@6.6.7: + resolution: {integrity: sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==} + engines: {npm: '>=2.0.0'} + safe-array-concat@1.1.3: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} @@ -4218,6 +4780,9 @@ packages: sentence-case@3.0.4: resolution: {integrity: sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==} + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + set-cookie-parser@2.7.2: resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} @@ -4261,10 +4826,16 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + simple-git@3.36.0: + resolution: {integrity: sha512-cGQjLjK8bxJw4QuYT7gxHw3/IouVESbhahSsHrX97MzCL1gu2u7oy38W6L2ZIGECEfIBG4BabsWDPjBxJENv9Q==} + slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -4303,6 +4874,9 @@ packages: sponge-case@1.0.1: resolution: {integrity: sha512-dblb9Et4DAtiZ5YSUZHLl4XhH4uK80GhAZrVXdN4O2P4gQ40Wa5UIOPUHlA/nFd2PLblBZWUioLMMAVrgpoYcA==} + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + sshpk@1.18.0: resolution: {integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==} engines: {node: '>=0.10.0'} @@ -4312,9 +4886,6 @@ packages: resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==} engines: {node: '>=12.0.0'} - stable-hash@0.0.5: - resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} - stealthy-require@1.1.1: resolution: {integrity: sha512-ZnWpYnYugiOVEY5GkcuJK1io5V8QmNYChG62gSit9pQVGErXtrKuPC55ITaVSukmMta5qpMU7vqLt2Lnni4f/g==} engines: {node: '>=0.10.0'} @@ -4329,6 +4900,14 @@ packages: string-template@1.0.0: resolution: {integrity: sha512-SLqR3GBUXuoPP5MmYtD7ompvXiG87QjT6lzOszyXjTM86Uu7At7vNnt2xgyTLq5o9T4IxTYFyGxcULqpsmsfdg==} + string-width@2.1.1: + resolution: {integrity: sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==} + engines: {node: '>=4'} + + string-width@3.1.0: + resolution: {integrity: sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==} + engines: {node: '>=6'} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -4364,6 +4943,14 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} + strip-ansi@4.0.0: + resolution: {integrity: sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==} + engines: {node: '>=4'} + + strip-ansi@5.2.0: + resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==} + engines: {node: '>=6'} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -4384,6 +4971,9 @@ packages: resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} engines: {node: '>=14.16'} + style-search@0.1.0: + resolution: {integrity: sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==} + stylelint-config-concentric@2.0.2: resolution: {integrity: sha512-R0d3GMB3FWyqNfhBlUiOXhOjzEzEbz2lBT/Kp8CMwbcB24rKtYB0Ot0jyIaCUqjjFcW05J2l3w2J9Oolwc9xyg==} peerDependencies: @@ -4395,6 +4985,12 @@ packages: peerDependencies: stylelint: ^17.0.0 + stylelint-config-standard@40.0.0: + resolution: {integrity: sha512-EznGJxOUhtWck2r6dJpbgAdPATIzvpLdK9+i5qPd4Lx70es66TkBPljSg4wN3Qnc6c4h2n+WbUrUynQ3fanjHw==} + engines: {node: '>=20.19.0'} + peerDependencies: + stylelint: ^17.0.0 + stylelint-no-unused-selectors@1.0.40: resolution: {integrity: sha512-NSx1OuW1a0xr9x6ms1RYY8TysBrsu3pfBdoY4HQ+p4DUvlrH7lE1ao+Bd7sxlqJClp12ocRK+b6mk+fD/cYrRg==} engines: {node: '>=8.16.0'} @@ -4418,10 +5014,17 @@ packages: engines: {node: '>=20.19.0'} hasBin: true + supercluster@7.1.5: + resolution: {integrity: sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg==} + supports-color@10.2.2: resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} engines: {node: '>=18'} + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -4434,6 +5037,9 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + svg-parser@2.0.4: + resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==} + svg-tags@1.0.0: resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==} @@ -4451,20 +5057,34 @@ packages: resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} engines: {node: '>=10.0.0'} - text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + terminal-size@4.0.1: + resolution: {integrity: sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ==} + engines: {node: '>=18'} + + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} timeout-signal@2.0.0: resolution: {integrity: sha512-YBGpG4bWsHoPvofT6y/5iqulfXIiIErl5B0LdtHT1mGXDFTAhhRrbUpTvBgYbovr+3cKblya2WAOcpoy90XguA==} engines: {node: '>=16'} + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinyqueue@2.0.3: + resolution: {integrity: sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==} + title-case@3.0.3: resolution: {integrity: sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==} + tmp@0.0.33: + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -4480,12 +5100,6 @@ packages: tr46@1.0.1: resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} - ts-api-utils@1.4.3: - resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} - engines: {node: '>=16'} - peerDependencies: - typescript: '>=4.2.0' - ts-api-utils@2.5.0: resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} @@ -4495,19 +5109,12 @@ packages: ts-log@2.2.7: resolution: {integrity: sha512-320x5Ggei84AxzlXp91QkIGSw5wgaLT6GeAH0KsqDmRZdVWW2OiSeVvElVoatk3f7nicwXlElXsoFkARiGE2yg==} - tsconfck@3.1.6: - resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} - engines: {node: ^18 || >=20} - hasBin: true - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true - tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + tslib@2.6.3: resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} @@ -4528,9 +5135,9 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} @@ -4565,9 +5172,6 @@ packages: engines: {node: '>=14.17'} hasBin: true - ufo@1.6.3: - resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} - unbash@2.2.0: resolution: {integrity: sha512-X2wH19RAPZE3+ldGicOkoj/SIA83OIxcJ6Cuaw23hf8Xc6fQpvZXY0SftE2JgS0QhYLUG4uwodSI3R53keyh7w==} engines: {node: '>=14'} @@ -4580,9 +5184,19 @@ packages: resolution: {integrity: sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==} engines: {node: '>=0.10.0'} + unconfig-core@7.5.0: + resolution: {integrity: sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==} + + unconfig@7.5.0: + resolution: {integrity: sha512-oi8Qy2JV4D3UQ0PsopR28CzdQ3S/5A1zwsUwp/rosSbfhJ5z7b90bIyTwi/F7hCLD4SGcZVjDzd4XoUQcEanvA==} + undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + unicorn-magic@0.4.0: resolution: {integrity: sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==} engines: {node: '>=20'} @@ -4609,6 +5223,9 @@ packages: upper-case@2.0.2: resolution: {integrity: sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==} + uri-js-replace@1.0.1: + resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==} + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -4653,13 +5270,50 @@ packages: resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} engines: {'0': node >=0.6.0} - vite-tsconfig-paths@6.1.1: - resolution: {integrity: sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg==} + vite-plugin-checker@0.13.0: + resolution: {integrity: sha512-14EkOZmfinVZNxRmg2uCNDwtqGc/33lU/UEJansHgu27+ad+r6mMBf1Xtnq57jGZWiO/xzwtiEKPYsganw7ZFQ==} + engines: {node: '>=16.11'} peerDependencies: - vite: '*' - - vite@8.0.5: - resolution: {integrity: sha512-nmu43Qvq9UopTRfMx2jOYW5l16pb3iDC1JH6yMuPkpVbzK0k+L7dfsEDH4jRgYFmsg0sTAqkojoZgzLMlwHsCQ==} + '@biomejs/biome': '>=1.7' + eslint: '>=9.39.4' + meow: ^13.2.0 || ^14.0.0 + optionator: ^0.9.4 + oxlint: '>=1' + stylelint: '>=16.26.1' + typescript: '*' + vite: '>=5.4.21' + vls: '*' + vti: '*' + vue-tsc: ~2.2.10 || ^3.0.0 + peerDependenciesMeta: + '@biomejs/biome': + optional: true + eslint: + optional: true + meow: + optional: true + optionator: + optional: true + oxlint: + optional: true + stylelint: + optional: true + typescript: + optional: true + vls: + optional: true + vti: + optional: true + vue-tsc: + optional: true + + vite-plugin-svgr@5.2.0: + resolution: {integrity: sha512-qj2eAKF8C6PZWemVTvQA0xgQIcP1hHU6Buh7fl6BhvayWwnuxE+z417miKxeDvRWbDrupQ1oK99hfxElopJ3sQ==} + peerDependencies: + vite: '>=3.0.0' + + vite@8.0.5: + resolution: {integrity: sha512-nmu43Qvq9UopTRfMx2jOYW5l16pb3iDC1JH6yMuPkpVbzK0k+L7dfsEDH4jRgYFmsg0sTAqkojoZgzLMlwHsCQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -4701,6 +5355,12 @@ packages: yaml: optional: true + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + + vt-pbf@3.1.3: + resolution: {integrity: sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==} + w3c-hr-time@1.0.2: resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==} deprecated: Use your platform's native performance.now() and performance.timeOrigin. @@ -4712,6 +5372,12 @@ packages: resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} engines: {node: 20 || >=22} + warning@4.0.3: + resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} + + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + web-streams-polyfill@3.3.3: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} @@ -4745,6 +5411,9 @@ packages: resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} engines: {node: '>= 0.4'} + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + which-typed-array@1.1.20: resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} engines: {node: '>= 0.4'} @@ -4758,6 +5427,9 @@ packages: engines: {node: '>= 8'} hasBin: true + window-post-message-proxy@0.3.0: + resolution: {integrity: sha512-dEpItkLX97djHvWzcbVmyelEPoq2Oglx2y5EYt+mCC0bwKvwbtE3ro3Zsg9heHAUir/3dHUBtV2Rr8DQQjwyPg==} + wonka@6.3.6: resolution: {integrity: sha512-MXH+6mDHAZ2GuMpgKS055FR6v0xVP3XwquxIMYXgiW+FejHQlMGlvVRZT4qMCxR+bEo/FCtIdKxwej9WV3YQag==} @@ -4765,6 +5437,14 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wrap-ansi@10.0.0: + resolution: {integrity: sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==} + engines: {node: '>=20'} + + wrap-ansi@5.1.0: + resolution: {integrity: sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==} + engines: {node: '>=6'} + wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -4784,6 +5464,14 @@ packages: resolution: {integrity: sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg==} engines: {node: ^20.17.0 || >=22.9.0} + write-yaml@1.0.0: + resolution: {integrity: sha512-QFB0QwNlUTSsICNb1HV+822MvFpTC1gtKcOfm0B9oqz4qOQXbRuMSxWPWryTEFBEZDWbI5zXabXArvShXTdLiA==} + engines: {node: '>=0.10.0'} + + write@0.3.3: + resolution: {integrity: sha512-e63bsTAFxFUU8OGClhjhhf2R72Njpq6DDTOFFBxDkfZFwoRRKZUx9rll6g/TvY0UcCdKE2OroYZje0v9ROzmfA==} + engines: {node: '>=0.10.0'} + ws@7.5.10: resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} engines: {node: '>=8.3.0'} @@ -4814,6 +5502,9 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -4821,15 +5512,24 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yaml-ast-parser@0.0.43: + resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==} + yaml@2.8.3: resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} engines: {node: '>= 14.6'} hasBin: true + yargs-parser@13.1.2: + resolution: {integrity: sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==} + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} + yargs@13.3.2: + resolution: {integrity: sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==} + yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} @@ -4842,21 +5542,12 @@ packages: resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} engines: {node: '>=18'} - zod-validation-error@3.5.4: - resolution: {integrity: sha512-+hEiRIiPobgyuFlEojnqjJnhFvg4r/i3cqgcm67eehZf/WBaK3g6cD02YU9mtdVxZjv8CzCA9n/Rhrs3yAAvAw==} - engines: {node: '>=18.0.0'} - peerDependencies: - zod: ^3.24.4 - zod-validation-error@4.0.2: resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} engines: {node: '>=18.0.0'} peerDependencies: zod: ^3.25.0 || ^4.0.0 - zod@3.25.76: - resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} - zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} @@ -4894,7 +5585,7 @@ snapshots: '@babel/types': 7.29.0 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -4909,10 +5600,6 @@ snapshots: '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 - '@babel/helper-annotate-as-pure@7.27.3': - dependencies: - '@babel/types': 7.29.0 - '@babel/helper-compilation-targets@7.28.6': dependencies: '@babel/compat-data': 7.29.0 @@ -4921,28 +5608,8 @@ snapshots: lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-create-class-features-plugin@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-member-expression-to-functions': 7.28.5 - '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/traverse': 7.29.0 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - '@babel/helper-globals@7.28.0': {} - '@babel/helper-member-expression-to-functions@7.28.5': - dependencies: - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - transitivePeerDependencies: - - supports-color - '@babel/helper-module-imports@7.28.6': dependencies: '@babel/traverse': 7.29.0 @@ -4959,28 +5626,8 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-optimise-call-expression@7.27.1': - dependencies: - '@babel/types': 7.29.0 - '@babel/helper-plugin-utils@7.28.6': {} - '@babel/helper-replace-supers@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-member-expression-to-functions': 7.28.5 - '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/traverse': 7.29.0 - transitivePeerDependencies: - - supports-color - - '@babel/helper-skip-transparent-expression-wrappers@7.27.1': - dependencies: - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - transitivePeerDependencies: - - supports-color - '@babel/helper-string-parser@7.27.1': {} '@babel/helper-validator-identifier@7.28.5': {} @@ -4996,14 +5643,6 @@ snapshots: dependencies: '@babel/types': 7.29.0 - '@babel/plugin-proposal-private-methods@7.18.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 - transitivePeerDependencies: - - supports-color - '@babel/plugin-syntax-import-assertions@7.28.6(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -5029,7 +5668,7 @@ snapshots: '@babel/parser': 7.29.2 '@babel/template': 7.28.6 '@babel/types': 7.29.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) transitivePeerDependencies: - supports-color @@ -5050,6 +5689,9 @@ snapshots: hashery: 1.5.1 keyv: 5.6.0 + '@colors/colors@1.5.0': + optional: true + '@csstools/cascade-layer-name-parser@3.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': dependencies: '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) @@ -5409,11 +6051,6 @@ snapshots: '@whatwg-node/promise-helpers': 1.3.2 tslib: 2.8.1 - '@eslint-community/eslint-utils@4.9.1(eslint@8.57.1)': - dependencies: - eslint: 8.57.1 - eslint-visitor-keys: 3.4.3 - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.6.1))': dependencies: eslint: 9.39.4(jiti@2.6.1) @@ -5424,7 +6061,7 @@ snapshots: '@eslint/config-array@0.21.2': dependencies: '@eslint/object-schema': 2.1.7 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) minimatch: 3.1.5 transitivePeerDependencies: - supports-color @@ -5437,24 +6074,14 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@2.1.4': + '@eslint/core@1.2.1': dependencies: - ajv: 6.14.0 - debug: 4.4.3 - espree: 9.6.1 - globals: 13.24.0 - ignore: 5.3.2 - import-fresh: 3.3.1 - js-yaml: 4.1.1 - minimatch: 3.1.5 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color + '@types/json-schema': 7.0.15 '@eslint/eslintrc@3.3.5': dependencies: ajv: 6.14.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 @@ -5465,10 +6092,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@8.57.1': {} - '@eslint/js@9.39.4': {} + '@eslint/json@1.2.0': + dependencies: + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.6.1 + '@humanwhocodes/momoa': 3.3.10 + natural-compare: 1.4.0 + '@eslint/object-schema@2.1.7': {} '@eslint/plugin-kit@0.4.1': @@ -5476,6 +6108,11 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 + '@eslint/plugin-kit@0.6.1': + dependencies: + '@eslint/core': 1.2.1 + levn: 0.4.1 + '@fastify/busboy@3.2.0': {} '@graphql-codegen/add@6.0.0(graphql@16.13.2)': @@ -5640,6 +6277,28 @@ snapshots: parse-filepath: 1.0.2 tslib: 2.6.3 + '@graphql-eslint/eslint-plugin@4.4.0(@types/node@24.12.2)(eslint@9.39.4(jiti@2.6.1))(graphql@16.13.2)(typescript@6.0.2)': + dependencies: + '@graphql-tools/code-file-loader': 8.1.30(graphql@16.13.2) + '@graphql-tools/graphql-tag-pluck': 8.3.29(graphql@16.13.2) + '@graphql-tools/utils': 10.11.0(graphql@16.13.2) + debug: 4.4.3(supports-color@10.2.2) + eslint: 9.39.4(jiti@2.6.1) + fast-glob: 3.3.3 + graphql: 16.13.2 + graphql-config: 5.1.6(@types/node@24.12.2)(graphql@16.13.2)(typescript@6.0.2) + graphql-depth-limit: 1.1.0(graphql@16.13.2) + lodash.lowercase: 4.3.0 + transitivePeerDependencies: + - '@fastify/websocket' + - '@types/node' + - bufferutil + - cosmiconfig-toml-loader + - crossws + - supports-color + - typescript + - utf-8-validate + '@graphql-hive/signal@2.0.0': {} '@graphql-tools/apollo-engine-loader@8.0.28(graphql@16.13.2)': @@ -5864,6 +6523,14 @@ snapshots: - crossws - utf-8-validate + '@graphql-tools/utils@10.11.0(graphql@16.13.2)': + dependencies: + '@graphql-typed-document-node/core': 3.2.0(graphql@16.13.2) + '@whatwg-node/promise-helpers': 1.3.2 + cross-inspect: 1.0.1 + graphql: 16.13.2 + tslib: 2.8.1 + '@graphql-tools/utils@11.0.0(graphql@16.13.2)': dependencies: '@graphql-typed-document-node/core': 3.2.0(graphql@16.13.2) @@ -5892,17 +6559,9 @@ snapshots: '@humanfs/core': 0.19.1 '@humanwhocodes/retry': 0.4.3 - '@humanwhocodes/config-array@0.13.0': - dependencies: - '@humanwhocodes/object-schema': 2.0.3 - debug: 4.4.3 - minimatch: 3.1.5 - transitivePeerDependencies: - - supports-color - '@humanwhocodes/module-importer@1.0.1': {} - '@humanwhocodes/object-schema@2.0.3': {} + '@humanwhocodes/momoa@3.3.10': {} '@humanwhocodes/retry@0.4.3': {} @@ -6074,6 +6733,87 @@ snapshots: '@keyv/serialize@1.1.1': {} + '@kwsites/file-exists@1.1.1': + dependencies: + debug: 4.4.3(supports-color@10.2.2) + transitivePeerDependencies: + - supports-color + + '@kwsites/promise-deferred@1.1.1': {} + + '@mapbox/geojson-rewind@0.5.2': + dependencies: + get-stream: 6.0.1 + minimist: 1.2.8 + + '@mapbox/geojson-types@1.0.2': {} + + '@mapbox/jsonlint-lines-primitives@2.0.2': {} + + '@mapbox/mapbox-gl-supported@1.5.0(mapbox-gl@1.13.3)': + dependencies: + mapbox-gl: 1.13.3 + + '@mapbox/point-geometry@0.1.0': {} + + '@mapbox/tiny-sdf@1.2.5': {} + + '@mapbox/unitbezier@0.0.0': {} + + '@mapbox/vector-tile@1.3.1': + dependencies: + '@mapbox/point-geometry': 0.1.0 + + '@mapbox/whoots-js@3.1.0': {} + + '@napi-rs/canvas-android-arm64@0.1.100': + optional: true + + '@napi-rs/canvas-darwin-arm64@0.1.100': + optional: true + + '@napi-rs/canvas-darwin-x64@0.1.100': + optional: true + + '@napi-rs/canvas-linux-arm-gnueabihf@0.1.100': + optional: true + + '@napi-rs/canvas-linux-arm64-gnu@0.1.100': + optional: true + + '@napi-rs/canvas-linux-arm64-musl@0.1.100': + optional: true + + '@napi-rs/canvas-linux-riscv64-gnu@0.1.100': + optional: true + + '@napi-rs/canvas-linux-x64-gnu@0.1.100': + optional: true + + '@napi-rs/canvas-linux-x64-musl@0.1.100': + optional: true + + '@napi-rs/canvas-win32-arm64-msvc@0.1.100': + optional: true + + '@napi-rs/canvas-win32-x64-msvc@0.1.100': + optional: true + + '@napi-rs/canvas@0.1.100': + optionalDependencies: + '@napi-rs/canvas-android-arm64': 0.1.100 + '@napi-rs/canvas-darwin-arm64': 0.1.100 + '@napi-rs/canvas-darwin-x64': 0.1.100 + '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.100 + '@napi-rs/canvas-linux-arm64-gnu': 0.1.100 + '@napi-rs/canvas-linux-arm64-musl': 0.1.100 + '@napi-rs/canvas-linux-riscv64-gnu': 0.1.100 + '@napi-rs/canvas-linux-x64-gnu': 0.1.100 + '@napi-rs/canvas-linux-x64-musl': 0.1.100 + '@napi-rs/canvas-win32-arm64-msvc': 0.1.100 + '@napi-rs/canvas-win32-x64-msvc': 0.1.100 + optional: true + '@napi-rs/wasm-runtime@0.2.12': dependencies: '@emnapi/core': 1.9.2 @@ -6100,8 +6840,6 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 - '@nolyfill/is-core-module@1.0.39': {} - '@oxc-parser/binding-android-arm-eabi@0.121.0': optional: true @@ -6236,6 +6974,51 @@ snapshots: '@oxc-resolver/binding-win32-x64-msvc@11.19.1': optional: true + '@poppinss/cliui@6.8.1': + dependencies: + '@poppinss/colors': 4.1.6 + cli-boxes: 4.0.1 + cli-table3: 0.6.5 + cli-truncate: 5.2.0 + log-update: 7.2.0 + pretty-hrtime: 1.0.3 + string-width: 8.2.0 + supports-color: 10.2.2 + terminal-size: 4.0.1 + + '@poppinss/colors@4.1.6': + dependencies: + kleur: 4.1.5 + + '@poppinss/validator-lite@2.1.2': {} + + '@quansync/fs@1.0.0': + dependencies: + quansync: 1.0.0 + + '@redocly/ajv@8.11.2': + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js-replace: 1.0.1 + + '@redocly/config@0.22.0': {} + + '@redocly/openapi-core@1.34.14(supports-color@10.2.2)': + dependencies: + '@redocly/ajv': 8.11.2 + '@redocly/config': 0.22.0 + colorette: 1.4.0 + https-proxy-agent: 7.0.6(supports-color@10.2.2) + js-levenshtein: 1.1.6 + js-yaml: 4.1.1 + minimatch: 5.1.9 + pluralize: 8.0.0 + yaml-ast-parser: 0.0.43 + transitivePeerDependencies: + - supports-color + '@repeaterjs/repeater@3.0.6': {} '@rolldown/binding-android-arm64@1.0.0-rc.12': @@ -6301,96 +7084,260 @@ snapshots: '@rolldown/pluginutils@1.0.0-rc.7': {} + '@rollup/pluginutils@5.3.0': + dependencies: + '@types/estree': 1.0.8 + estree-walker: 2.0.2 + picomatch: 4.0.4 + '@rtsao/scc@1.1.0': {} - '@sindresorhus/merge-streams@4.0.0': {} + '@sentry-internal/browser-utils@10.49.0': + dependencies: + '@sentry/core': 10.49.0 - '@stylistic/eslint-plugin-js@1.8.1(eslint@8.57.1)': + '@sentry-internal/feedback@10.49.0': dependencies: - '@types/eslint': 8.56.12 - acorn: 8.16.0 - escape-string-regexp: 4.0.0 - eslint: 8.57.1 - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 + '@sentry/core': 10.49.0 - '@stylistic/eslint-plugin-jsx@1.8.1(eslint@8.57.1)': + '@sentry-internal/replay-canvas@10.49.0': dependencies: - '@stylistic/eslint-plugin-js': 1.8.1(eslint@8.57.1) - '@types/eslint': 8.56.12 - eslint: 8.57.1 - estraverse: 5.3.0 - picomatch: 4.0.4 + '@sentry-internal/replay': 10.49.0 + '@sentry/core': 10.49.0 - '@stylistic/eslint-plugin-plus@1.8.1(eslint@8.57.1)(typescript@6.0.2)': + '@sentry-internal/replay@10.49.0': dependencies: - '@types/eslint': 8.56.12 - '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@6.0.2) - eslint: 8.57.1 - transitivePeerDependencies: - - supports-color - - typescript + '@sentry-internal/browser-utils': 10.49.0 + '@sentry/core': 10.49.0 - '@stylistic/eslint-plugin-ts@1.8.1(eslint@8.57.1)(typescript@6.0.2)': + '@sentry/browser@10.49.0': dependencies: - '@stylistic/eslint-plugin-js': 1.8.1(eslint@8.57.1) - '@types/eslint': 8.56.12 - '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@6.0.2) - eslint: 8.57.1 - transitivePeerDependencies: - - supports-color - - typescript + '@sentry-internal/browser-utils': 10.49.0 + '@sentry-internal/feedback': 10.49.0 + '@sentry-internal/replay': 10.49.0 + '@sentry-internal/replay-canvas': 10.49.0 + '@sentry/core': 10.49.0 + + '@sentry/core@10.49.0': {} - '@stylistic/eslint-plugin@1.8.1(eslint@8.57.1)(typescript@6.0.2)': + '@sentry/react@10.49.0(react@19.2.4)': dependencies: - '@stylistic/eslint-plugin-js': 1.8.1(eslint@8.57.1) - '@stylistic/eslint-plugin-jsx': 1.8.1(eslint@8.57.1) - '@stylistic/eslint-plugin-plus': 1.8.1(eslint@8.57.1)(typescript@6.0.2) - '@stylistic/eslint-plugin-ts': 1.8.1(eslint@8.57.1)(typescript@6.0.2) - '@types/eslint': 8.56.12 - eslint: 8.57.1 - transitivePeerDependencies: - - supports-color - - typescript + '@sentry/browser': 10.49.0 + '@sentry/core': 10.49.0 + react: 19.2.4 - '@togglecorp/fujs@2.2.0': + '@simple-git/args-pathspec@1.0.3': {} + + '@simple-git/argv-parser@1.1.1': dependencies: - '@babel/runtime-corejs3': 7.29.2 + '@simple-git/args-pathspec': 1.0.3 - '@tybys/wasm-util@0.10.1': + '@sindresorhus/merge-streams@4.0.0': {} + + '@standard-schema/spec@1.1.0': {} + + '@stylistic/stylelint-plugin@5.1.0(stylelint@17.6.0(typescript@6.0.2))': dependencies: - tslib: 2.8.1 - optional: true + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + '@csstools/media-query-list-parser': 5.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + postcss: 8.5.9 + postcss-selector-parser: 7.1.1 + postcss-value-parser: 4.2.0 + style-search: 0.1.0 + stylelint: 17.6.0(typescript@6.0.2) - '@types/babel__core@7.20.5': + '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.29.0)': dependencies: - '@babel/parser': 7.29.2 - '@babel/types': 7.29.0 - '@types/babel__generator': 7.27.0 - '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.28.0 + '@babel/core': 7.29.0 - '@types/babel__generator@7.27.0': + '@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.29.0)': dependencies: - '@babel/types': 7.29.0 + '@babel/core': 7.29.0 - '@types/babel__template@7.4.4': + '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.29.0)': dependencies: - '@babel/parser': 7.29.2 - '@babel/types': 7.29.0 + '@babel/core': 7.29.0 - '@types/babel__traverse@7.28.0': + '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.29.0)': dependencies: - '@babel/types': 7.29.0 + '@babel/core': 7.29.0 - '@types/eslint@8.56.12': + '@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.29.0)': dependencies: - '@types/estree': 1.0.8 - '@types/json-schema': 7.0.15 + '@babel/core': 7.29.0 - '@types/estree@1.0.8': {} + '@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 - '@types/hoist-non-react-statics@3.3.7(@types/react@19.2.14)': + '@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + + '@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + + '@svgr/babel-preset@8.1.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.29.0) + '@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.29.0) + '@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.29.0) + '@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.29.0) + '@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.29.0) + '@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.29.0) + '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.29.0) + '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.29.0) + + '@svgr/core@8.1.0(typescript@6.0.2)': + dependencies: + '@babel/core': 7.29.0 + '@svgr/babel-preset': 8.1.0(@babel/core@7.29.0) + camelcase: 6.3.0 + cosmiconfig: 8.3.6(typescript@6.0.2) + snake-case: 3.0.4 + transitivePeerDependencies: + - supports-color + - typescript + + '@svgr/hast-util-to-babel-ast@8.0.0': + dependencies: + '@babel/types': 7.29.0 + entities: 4.5.0 + + '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@6.0.2))': + dependencies: + '@babel/core': 7.29.0 + '@svgr/babel-preset': 8.1.0(@babel/core@7.29.0) + '@svgr/core': 8.1.0(typescript@6.0.2) + '@svgr/hast-util-to-babel-ast': 8.0.0 + svg-parser: 2.0.4 + transitivePeerDependencies: + - supports-color + + '@togglecorp/fujs@2.2.0': + dependencies: + '@babel/runtime-corejs3': 7.29.2 + + '@togglecorp/re-map@0.3.0(mapbox-gl@1.13.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@babel/runtime-corejs3': 7.29.2 + '@togglecorp/fujs': 2.2.0 + mapbox-gl: 1.13.3 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@togglecorp/toggle-form@2.0.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@babel/runtime-corejs3': 7.29.2 + '@togglecorp/fujs': 2.2.0 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@togglecorp/toggle-request@1.0.0-beta.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@babel/runtime-corejs3': 7.29.2 + '@togglecorp/fujs': 2.2.0 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@togglecorp/vite-plugin-validate-env@2.2.1(vite@8.0.5(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.2)(jiti@2.6.1)(yaml@2.8.3))': + dependencies: + '@poppinss/cliui': 6.8.1 + '@poppinss/validator-lite': 2.1.2 + '@standard-schema/spec': 1.1.0 + unconfig: 7.5.0 + vite: 8.0.5(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.2)(jiti@2.6.1)(yaml@2.8.3) + + '@turf/bbox@7.3.5': + dependencies: + '@turf/helpers': 7.3.5 + '@turf/meta': 7.3.5 + '@types/geojson': 7946.0.16 + tslib: 2.8.1 + + '@turf/buffer@7.3.5': + dependencies: + '@turf/bbox': 7.3.5 + '@turf/center': 7.3.5 + '@turf/helpers': 7.3.5 + '@turf/jsts': 2.7.2 + '@turf/meta': 7.3.5 + '@turf/projection': 7.3.5 + '@types/geojson': 7946.0.16 + d3-geo: 1.7.1 + + '@turf/center@7.3.5': + dependencies: + '@turf/bbox': 7.3.5 + '@turf/helpers': 7.3.5 + '@types/geojson': 7946.0.16 + tslib: 2.8.1 + + '@turf/clone@7.3.5': + dependencies: + '@turf/helpers': 7.3.5 + '@types/geojson': 7946.0.16 + tslib: 2.8.1 + + '@turf/helpers@7.3.5': + dependencies: + '@types/geojson': 7946.0.16 + tslib: 2.8.1 + + '@turf/jsts@2.7.2': + dependencies: + jsts: 2.7.1 + + '@turf/meta@7.3.5': + dependencies: + '@turf/helpers': 7.3.5 + '@types/geojson': 7946.0.16 + tslib: 2.8.1 + + '@turf/projection@7.3.5': + dependencies: + '@turf/clone': 7.3.5 + '@turf/helpers': 7.3.5 + '@turf/meta': 7.3.5 + '@types/geojson': 7946.0.16 + tslib: 2.8.1 + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/estree@1.0.8': {} + + '@types/file-saver@2.0.7': {} + + '@types/geojson@7946.0.16': {} + + '@types/hoist-non-react-statics@3.3.7(@types/react@19.2.14)': dependencies: '@types/react': 19.2.14 hoist-non-react-statics: 3.3.2 @@ -6399,10 +7346,18 @@ snapshots: '@types/json5@0.0.29': {} + '@types/mapbox-gl@1.13.10': + dependencies: + '@types/geojson': 7946.0.16 + '@types/node@24.12.2': dependencies: undici-types: 7.16.0 + '@types/papaparse@5.5.2': + dependencies: + '@types/node': 24.12.2 + '@types/react-dom@19.2.3(@types/react@19.2.14)': dependencies: '@types/react': 19.2.14 @@ -6411,32 +7366,10 @@ snapshots: dependencies: csstype: 3.2.3 - '@types/semver@7.7.1': {} - '@types/ws@8.18.1': dependencies: '@types/node': 24.12.2 - '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@6.0.2))(eslint@8.57.1)(typescript@6.0.2)': - dependencies: - '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@6.0.2) - '@typescript-eslint/scope-manager': 6.21.0 - '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.1)(typescript@6.0.2) - '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@6.0.2) - '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.3 - eslint: 8.57.1 - graphemer: 1.4.0 - ignore: 5.3.2 - natural-compare: 1.4.0 - semver: 7.7.4 - ts-api-utils: 1.4.3(typescript@6.0.2) - optionalDependencies: - typescript: 6.0.2 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -6453,26 +7386,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@6.0.2)': - dependencies: - '@typescript-eslint/scope-manager': 6.21.0 - '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/typescript-estree': 6.21.0(typescript@6.0.2) - '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.3 - eslint: 8.57.1 - optionalDependencies: - typescript: 6.0.2 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2)': dependencies: '@typescript-eslint/scope-manager': 8.58.0 '@typescript-eslint/types': 8.58.0 '@typescript-eslint/typescript-estree': 8.58.0(typescript@6.0.2) '@typescript-eslint/visitor-keys': 8.58.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) eslint: 9.39.4(jiti@2.6.1) typescript: 6.0.2 transitivePeerDependencies: @@ -6482,16 +7402,11 @@ snapshots: dependencies: '@typescript-eslint/tsconfig-utils': 8.58.0(typescript@6.0.2) '@typescript-eslint/types': 8.58.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@6.21.0': - dependencies: - '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/visitor-keys': 6.21.0 - '@typescript-eslint/scope-manager@8.58.0': dependencies: '@typescript-eslint/types': 8.58.0 @@ -6501,56 +7416,27 @@ snapshots: dependencies: typescript: 6.0.2 - '@typescript-eslint/type-utils@6.21.0(eslint@8.57.1)(typescript@6.0.2)': - dependencies: - '@typescript-eslint/typescript-estree': 6.21.0(typescript@6.0.2) - '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@6.0.2) - debug: 4.4.3 - eslint: 8.57.1 - ts-api-utils: 1.4.3(typescript@6.0.2) - optionalDependencies: - typescript: 6.0.2 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/type-utils@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2)': dependencies: '@typescript-eslint/types': 8.58.0 '@typescript-eslint/typescript-estree': 8.58.0(typescript@6.0.2) '@typescript-eslint/utils': 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2) - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) eslint: 9.39.4(jiti@2.6.1) ts-api-utils: 2.5.0(typescript@6.0.2) typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@6.21.0': {} - '@typescript-eslint/types@8.58.0': {} - '@typescript-eslint/typescript-estree@6.21.0(typescript@6.0.2)': - dependencies: - '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.3 - globby: 11.1.0 - is-glob: 4.0.3 - minimatch: 9.0.3 - semver: 7.7.4 - ts-api-utils: 1.4.3(typescript@6.0.2) - optionalDependencies: - typescript: 6.0.2 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/typescript-estree@8.58.0(typescript@6.0.2)': dependencies: '@typescript-eslint/project-service': 8.58.0(typescript@6.0.2) '@typescript-eslint/tsconfig-utils': 8.58.0(typescript@6.0.2) '@typescript-eslint/types': 8.58.0 '@typescript-eslint/visitor-keys': 8.58.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) minimatch: 10.2.5 semver: 7.7.4 tinyglobby: 0.2.15 @@ -6559,20 +7445,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@6.21.0(eslint@8.57.1)(typescript@6.0.2)': - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) - '@types/json-schema': 7.0.15 - '@types/semver': 7.7.1 - '@typescript-eslint/scope-manager': 6.21.0 - '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/typescript-estree': 6.21.0(typescript@6.0.2) - eslint: 8.57.1 - semver: 7.7.4 - transitivePeerDependencies: - - supports-color - - typescript - '@typescript-eslint/utils@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) @@ -6584,18 +7456,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@6.21.0': - dependencies: - '@typescript-eslint/types': 6.21.0 - eslint-visitor-keys: 3.4.3 - '@typescript-eslint/visitor-keys@8.58.0': dependencies: '@typescript-eslint/types': 8.58.0 eslint-visitor-keys: 5.0.1 - '@ungap/structured-clone@1.3.0': {} - '@unrs/resolver-binding-android-arm-eabi@1.11.1': optional: true @@ -6718,6 +7583,8 @@ snapshots: acorn@8.16.0: {} + agent-base@7.1.4: {} + ajv@6.14.0: dependencies: fast-deep-equal: 3.1.3 @@ -6732,20 +7599,36 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + ansi-colors@4.1.3: {} + + ansi-escapes@3.2.0: {} + ansi-escapes@7.3.0: dependencies: environment: 1.1.0 + ansi-regex@3.0.1: {} + + ansi-regex@4.1.1: {} + ansi-regex@5.0.1: {} ansi-regex@6.2.2: {} + ansi-styles@3.2.1: + dependencies: + color-convert: 1.9.3 + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 ansi-styles@6.2.3: {} + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + argparse@2.0.1: {} aria-hidden@1.2.6: @@ -6825,6 +7708,8 @@ snapshots: get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 + arrify@1.0.1: {} + asn1@0.2.6: dependencies: safer-buffer: 2.1.2 @@ -6881,7 +7766,7 @@ snapshots: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@2.0.3: + brace-expansion@2.1.0: dependencies: balanced-match: 1.0.2 @@ -6903,12 +7788,6 @@ snapshots: node-releases: 2.0.37 update-browserslist-db: 1.2.3(browserslist@4.28.2) - builtin-modules@3.3.0: {} - - builtins@5.1.0: - dependencies: - semver: 7.7.4 - cacheable@2.3.4: dependencies: '@cacheable/memory': 2.0.8 @@ -6941,6 +7820,10 @@ snapshots: pascal-case: 3.1.2 tslib: 2.8.1 + camelcase@5.3.1: {} + + camelcase@6.3.0: {} + caniuse-lite@1.0.30001786: {} capital-case@1.0.4: @@ -6951,6 +7834,12 @@ snapshots: caseless@0.12.0: {} + chalk@2.4.2: + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -6984,45 +7873,93 @@ snapshots: snake-case: 3.0.4 tslib: 2.8.1 + change-case@5.4.4: {} + + chardet@0.7.0: {} + chardet@2.1.1: {} + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + cli-boxes@4.0.1: {} + + cli-cursor@2.1.0: + dependencies: + restore-cursor: 2.0.0 + cli-cursor@5.0.0: dependencies: restore-cursor: 5.1.0 + cli-spinners@2.9.2: {} + + cli-table3@0.6.5: + dependencies: + string-width: 4.2.3 + optionalDependencies: + '@colors/colors': 1.5.0 + + cli-table@0.3.11: + dependencies: + colors: 1.0.3 + cli-truncate@5.2.0: dependencies: slice-ansi: 8.0.0 string-width: 8.2.0 + cli-width@2.2.1: {} + cli-width@4.1.0: {} + cliui@5.0.0: + dependencies: + string-width: 3.1.0 + strip-ansi: 5.2.0 + wrap-ansi: 5.1.0 + cliui@8.0.1: dependencies: string-width: 4.2.3 strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + clone@1.0.4: {} + + clsx@2.1.1: {} + + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + color-convert@2.0.1: dependencies: color-name: 1.1.4 + color-name@1.1.3: {} + color-name@1.1.4: {} colord@2.9.3: {} + colorette@1.4.0: {} + colorette@2.0.20: {} + colors@1.0.3: {} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 + commander@2.20.3: {} + common-tags@1.8.2: {} concat-map@0.0.1: {} - confbox@0.1.8: {} - confusing-browser-globals@1.0.11: {} constant-case@3.0.4: @@ -7090,6 +8027,8 @@ snapshots: mdn-data: 2.27.1 source-map-js: 1.2.1 + csscolorparser@1.0.3: {} + cssdb@8.8.0: {} cssesc@3.0.0: {} @@ -7104,6 +8043,12 @@ snapshots: csstype@3.2.3: {} + d3-array@1.2.4: {} + + d3-geo@1.7.1: + dependencies: + d3-array: 1.2.4 + damerau-levenshtein@1.0.8: {} dashdash@1.14.1: @@ -7144,14 +8089,22 @@ snapshots: dependencies: ms: 2.1.3 - debug@4.4.3: + debug@4.4.3(supports-color@10.2.2): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 10.2.2 + + decamelize@1.2.0: {} deep-is@0.1.4: {} deepmerge@4.3.1: {} + defaults@1.0.4: + dependencies: + clone: 1.0.4 + define-data-property@1.1.4: dependencies: es-define-property: 1.0.1 @@ -7164,10 +8117,14 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + defu@6.1.7: {} + delayed-stream@1.0.0: {} dependency-graph@1.0.0: {} + dequal@2.0.3: {} + detect-indent@6.1.0: {} detect-libc@2.1.2: {} @@ -7182,10 +8139,6 @@ snapshots: dependencies: esutils: 2.0.3 - doctrine@3.0.0: - dependencies: - esutils: 2.0.3 - dom-serializer@2.0.0: dependencies: domelementtype: 2.3.0 @@ -7234,6 +8187,8 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + earcut@2.2.4: {} + ecc-jsbn@0.1.2: dependencies: jsbn: 0.1.1 @@ -7243,6 +8198,8 @@ snapshots: emoji-regex@10.6.0: {} + emoji-regex@7.0.3: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -7361,8 +8318,12 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 + es6-promise@3.3.1: {} + escalade@3.2.0: {} + escape-string-regexp@1.0.5: {} + escape-string-regexp@4.0.0: {} escodegen@1.14.3: @@ -7374,11 +8335,6 @@ snapshots: optionalDependencies: source-map: 0.6.1 - eslint-compat-utils@0.5.1(eslint@8.57.1): - dependencies: - eslint: 8.57.1 - semver: 7.7.4 - eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)): dependencies: confusing-browser-globals: 1.0.11 @@ -7388,24 +8344,6 @@ snapshots: object.entries: 1.1.9 semver: 6.3.1 - eslint-config-airbnb-flat@0.0.12(eslint-plugin-import@2.32.0)(typescript@6.0.2): - dependencies: - '@stylistic/eslint-plugin': 1.8.1(eslint@8.57.1)(typescript@6.0.2) - '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@6.0.2))(eslint@8.57.1)(typescript@6.0.2) - '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@6.0.2) - eslint: 8.57.1 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1) - eslint-plugin-i: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@6.0.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-n: 16.6.2(eslint@8.57.1) - globals: 13.24.0 - local-pkg: 0.5.1 - transitivePeerDependencies: - - eslint-import-resolver-webpack - - eslint-plugin-import - - eslint-plugin-import-x - - supports-color - - typescript - eslint-config-airbnb@19.0.4(eslint-plugin-import@2.32.0)(eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.4(jiti@2.6.1)))(eslint-plugin-react-hooks@7.0.1(eslint@9.39.4(jiti@2.6.1)))(eslint-plugin-react@7.37.5(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)): dependencies: eslint: 9.39.4(jiti@2.6.1) @@ -7432,24 +8370,9 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1): - dependencies: - '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.3 - eslint: 8.57.1 - get-tsconfig: 4.13.7 - is-bun-module: 2.0.0 - stable-hash: 0.0.5 - tinyglobby: 0.2.15 - unrs-resolver: 1.11.1 - optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@2.6.1)) - transitivePeerDependencies: - - supports-color - eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) eslint: 9.39.4(jiti@2.6.1) eslint-import-context: 0.1.9(unrs-resolver@1.11.1) get-tsconfig: 4.13.7 @@ -7462,17 +8385,6 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@6.0.2))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1))(eslint@8.57.1): - dependencies: - debug: 3.2.7 - optionalDependencies: - '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@6.0.2) - eslint: 8.57.1 - eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1) - transitivePeerDependencies: - - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@2.6.1)): dependencies: debug: 3.2.7 @@ -7484,30 +8396,6 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-es-x@7.8.0(eslint@8.57.1): - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) - '@eslint-community/regexpp': 4.12.2 - eslint: 8.57.1 - eslint-compat-utils: 0.5.1(eslint@8.57.1) - - eslint-plugin-i@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@6.0.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1))(eslint@8.57.1): - dependencies: - debug: 4.4.3 - doctrine: 3.0.0 - eslint: 8.57.1 - eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@6.0.2))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1))(eslint@8.57.1) - get-tsconfig: 4.13.7 - is-glob: 4.0.3 - minimatch: 3.1.5 - semver: 7.7.4 - transitivePeerDependencies: - - '@typescript-eslint/parser' - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - eslint-plugin-import-exports-imports-resolver@1.0.1: dependencies: resolve.exports: 1.1.1 @@ -7565,33 +8453,6 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-n@16.6.2(eslint@8.57.1): - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) - builtins: 5.1.0 - eslint: 8.57.1 - eslint-plugin-es-x: 7.8.0(eslint@8.57.1) - get-tsconfig: 4.13.7 - globals: 13.24.0 - ignore: 5.3.2 - is-builtin-module: 3.2.1 - is-core-module: 2.16.1 - minimatch: 3.1.5 - resolve: 1.22.11 - semver: 7.7.4 - - eslint-plugin-react-compiler@19.1.0-rc.2(eslint@9.39.4(jiti@2.6.1)): - dependencies: - '@babel/core': 7.29.0 - '@babel/parser': 7.29.2 - '@babel/plugin-proposal-private-methods': 7.18.6(@babel/core@7.29.0) - eslint: 9.39.4(jiti@2.6.1) - hermes-parser: 0.25.1 - zod: 3.25.76 - zod-validation-error: 3.5.4(zod@3.25.76) - transitivePeerDependencies: - - supports-color - eslint-plugin-react-hooks@7.0.1(eslint@9.39.4(jiti@2.6.1)): dependencies: '@babel/core': 7.29.0 @@ -7603,7 +8464,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-react-refresh@0.5.2(eslint@9.39.4(jiti@2.6.1)): + eslint-plugin-react-refresh@0.4.26(eslint@9.39.4(jiti@2.6.1)): dependencies: eslint: 9.39.4(jiti@2.6.1) @@ -7633,64 +8494,16 @@ snapshots: dependencies: eslint: 9.39.4(jiti@2.6.1) - eslint-scope@7.2.2: - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - - eslint-scope@8.4.0: - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - - eslint-visitor-keys@3.4.3: {} - - eslint-visitor-keys@4.2.1: {} - - eslint-visitor-keys@5.0.1: {} - - eslint@8.57.1: - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) - '@eslint-community/regexpp': 4.12.2 - '@eslint/eslintrc': 2.1.4 - '@eslint/js': 8.57.1 - '@humanwhocodes/config-array': 0.13.0 - '@humanwhocodes/module-importer': 1.0.1 - '@nodelib/fs.walk': 1.2.8 - '@ungap/structured-clone': 1.3.0 - ajv: 6.14.0 - chalk: 4.1.2 - cross-spawn: 7.0.6 - debug: 4.4.3 - doctrine: 3.0.0 - escape-string-regexp: 4.0.0 - eslint-scope: 7.2.2 - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 - esquery: 1.7.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 6.0.1 - find-up: 5.0.0 - glob-parent: 6.0.2 - globals: 13.24.0 - graphemer: 1.4.0 - ignore: 5.3.2 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - is-path-inside: 3.0.3 - js-yaml: 4.1.1 - json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 - lodash.merge: 4.6.2 - minimatch: 3.1.5 - natural-compare: 1.4.0 - optionator: 0.9.4 - strip-ansi: 6.0.1 - text-table: 0.2.0 - transitivePeerDependencies: - - supports-color + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} eslint@9.39.4(jiti@2.6.1): dependencies: @@ -7709,7 +8522,7 @@ snapshots: ajv: 6.14.0 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) escape-string-regexp: 4.0.0 eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -7739,12 +8552,6 @@ snapshots: acorn-jsx: 5.3.2(acorn@8.16.0) eslint-visitor-keys: 4.2.1 - espree@9.6.1: - dependencies: - acorn: 8.16.0 - acorn-jsx: 5.3.2(acorn@8.16.0) - eslint-visitor-keys: 3.4.3 - esprima@4.0.1: {} esquery@1.7.0: @@ -7759,12 +8566,24 @@ snapshots: estraverse@5.3.0: {} + estree-walker@2.0.2: {} + esutils@2.0.3: {} eventemitter3@5.0.4: {} + extend-shallow@2.0.1: + dependencies: + is-extendable: 0.1.1 + extend@3.0.2: {} + external-editor@3.1.0: + dependencies: + chardet: 0.7.0 + iconv-lite: 0.4.24 + tmp: 0.0.33 + extsprintf@1.3.0: {} fast-deep-equal@3.1.3: {} @@ -7802,33 +8621,33 @@ snapshots: node-domexception: 1.0.0 web-streams-polyfill: 3.3.3 - file-entry-cache@11.1.2: + figures@2.0.0: dependencies: - flat-cache: 6.1.22 + escape-string-regexp: 1.0.5 - file-entry-cache@6.0.1: + file-entry-cache@11.1.2: dependencies: - flat-cache: 3.2.0 + flat-cache: 6.1.22 file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 + file-saver@2.0.5: {} + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 + find-up@3.0.0: + dependencies: + locate-path: 3.0.0 + find-up@5.0.0: dependencies: locate-path: 6.0.0 path-exists: 4.0.0 - flat-cache@3.2.0: - dependencies: - flatted: 3.4.2 - keyv: 4.5.4 - rimraf: 3.0.2 - flat-cache@4.0.1: dependencies: flatted: 3.4.2 @@ -7868,6 +8687,8 @@ snapshots: fraction.js@5.3.4: {} + fs-exists-sync@0.1.0: {} + fs.realpath@1.0.0: {} fsevents@2.3.3: @@ -7890,6 +8711,8 @@ snapshots: gensync@1.0.0-beta.2: {} + geojson-vt@3.2.1: {} + get-caller-file@2.0.5: {} get-east-asian-width@1.5.0: {} @@ -7914,6 +8737,8 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 + get-stream@6.0.1: {} + get-symbol-description@1.1.0: dependencies: call-bound: 1.0.4 @@ -7928,6 +8753,8 @@ snapshots: dependencies: assert-plus: 1.0.0 + gl-matrix@3.4.4: {} + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -7955,10 +8782,6 @@ snapshots: kind-of: 6.0.3 which: 1.3.1 - globals@13.24.0: - dependencies: - type-fest: 0.20.2 - globals@14.0.0: {} globals@17.4.0: {} @@ -7988,11 +8811,9 @@ snapshots: globjoin@0.1.4: {} - globrex@0.1.2: {} - gopd@1.2.0: {} - graphemer@1.4.0: {} + graceful-fs@4.2.11: {} graphql-config@5.1.6(@types/node@24.12.2)(graphql@16.13.2)(typescript@6.0.2): dependencies: @@ -8016,6 +8837,11 @@ snapshots: - typescript - utf-8-validate + graphql-depth-limit@1.1.0(graphql@16.13.2): + dependencies: + arrify: 1.0.1 + graphql: 16.13.2 + graphql-tag@2.12.6(graphql@16.13.2): dependencies: graphql: 16.13.2 @@ -8029,6 +8855,8 @@ snapshots: graphql@16.13.2: {} + grid-index@1.1.0: {} + har-schema@2.0.0: {} har-validator@5.1.5: @@ -8038,6 +8866,8 @@ snapshots: has-bigints@1.1.0: {} + has-flag@3.0.0: {} + has-flag@4.0.0: {} has-flag@5.0.1: {} @@ -8096,12 +8926,23 @@ snapshots: domutils: 3.2.2 entities: 7.0.1 + http-post-message@0.3.0: + dependencies: + es6-promise: 3.3.1 + http-signature@1.2.0: dependencies: assert-plus: 1.0.0 jsprim: 1.4.2 sshpk: 1.18.0 + https-proxy-agent@7.0.6(supports-color@10.2.2): + dependencies: + agent-base: 7.1.4 + debug: 4.4.3(supports-color@10.2.2) + transitivePeerDependencies: + - supports-color + iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 @@ -8110,6 +8951,8 @@ snapshots: dependencies: safer-buffer: 2.1.2 + ieee754@1.2.1: {} + ignore@5.3.2: {} ignore@7.0.5: {} @@ -8127,6 +8970,8 @@ snapshots: imurmurhash@0.1.4: {} + index-to-position@1.2.0: {} + inflight@1.0.6: dependencies: once: 1.4.0 @@ -8136,6 +8981,22 @@ snapshots: ini@1.3.8: {} + inquirer@6.5.2: + dependencies: + ansi-escapes: 3.2.0 + chalk: 2.4.2 + cli-cursor: 2.1.0 + cli-width: 2.2.1 + external-editor: 3.1.0 + figures: 2.0.0 + lodash: 4.18.1 + mute-stream: 0.0.7 + run-async: 2.4.1 + rxjs: 6.6.7 + string-width: 2.1.1 + strip-ansi: 5.2.0 + through: 2.3.8 + internal-slot@1.1.0: dependencies: es-errors: 1.3.0 @@ -8178,10 +9039,6 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 - is-builtin-module@3.2.1: - dependencies: - builtin-modules: 3.3.0 - is-bun-module@2.0.0: dependencies: semver: 7.7.4 @@ -8203,12 +9060,16 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 + is-extendable@0.1.1: {} + is-extglob@2.1.1: {} is-finalizationregistry@1.1.1: dependencies: call-bound: 1.0.4 + is-fullwidth-code-point@2.0.0: {} + is-fullwidth-code-point@3.0.0: {} is-fullwidth-code-point@5.1.0: @@ -8242,8 +9103,6 @@ snapshots: is-number@7.0.0: {} - is-path-inside@3.0.3: {} - is-path-inside@4.0.0: {} is-plain-object@5.0.0: {} @@ -8330,8 +9189,15 @@ snapshots: jiti@2.6.1: {} + js-levenshtein@1.1.6: {} + js-tokens@4.0.0: {} + js-yaml@3.14.2: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + js-yaml@4.1.1: dependencies: argparse: 2.0.1 @@ -8404,6 +9270,8 @@ snapshots: json-schema: 0.4.0 verror: 1.10.0 + jsts@2.7.1: {} + jsx-ast-utils@3.3.5: dependencies: array-includes: 3.1.9 @@ -8411,6 +9279,8 @@ snapshots: object.assign: 4.1.7 object.values: 1.2.1 + kdbush@3.0.0: {} + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -8421,6 +9291,8 @@ snapshots: kind-of@6.0.3: {} + kleur@4.1.5: {} + knip@6.3.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2): dependencies: '@nodelib/fs.walk': 1.2.8 @@ -8509,6 +9381,24 @@ snapshots: lines-and-columns@1.2.4: {} + lint@0.8.19: + dependencies: + chalk: 2.4.2 + cli-table: 0.3.11 + commander: 2.20.3 + inquirer: 6.5.2 + js-yaml: 4.1.1 + loadash: 1.0.0 + moment: 2.30.1 + ora: 3.4.0 + prettier: 1.19.1 + replace-in-file: 3.4.4 + request: 2.88.2 + simple-git: 3.36.0 + write-yaml: 1.0.0 + transitivePeerDependencies: + - supports-color + listr2@9.0.5: dependencies: cli-truncate: 5.2.0 @@ -8518,15 +9408,21 @@ snapshots: rfdc: 1.4.1 wrap-ansi: 9.0.2 - local-pkg@0.5.1: + loadash@1.0.0: {} + + locate-path@3.0.0: dependencies: - mlly: 1.8.2 - pkg-types: 1.3.1 + p-locate: 3.0.0 + path-exists: 3.0.0 locate-path@6.0.0: dependencies: p-locate: 5.0.0 + lodash.isequal@4.5.0: {} + + lodash.lowercase@4.3.0: {} + lodash.merge@4.6.2: {} lodash.sortby@4.7.0: {} @@ -8535,6 +9431,10 @@ snapshots: lodash@4.18.1: {} + log-symbols@2.2.0: + dependencies: + chalk: 2.4.2 + log-symbols@4.1.0: dependencies: chalk: 4.1.2 @@ -8548,6 +9448,14 @@ snapshots: strip-ansi: 7.2.0 wrap-ansi: 9.0.2 + log-update@7.2.0: + dependencies: + ansi-escapes: 7.3.0 + cli-cursor: 5.0.0 + slice-ansi: 8.0.0 + strip-ansi: 7.2.0 + wrap-ansi: 10.0.0 + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -8564,8 +9472,37 @@ snapshots: dependencies: yallist: 3.1.1 + make-cancellable-promise@2.0.0: {} + + make-event-props@2.0.0: {} + map-cache@0.2.2: {} + mapbox-gl@1.13.3: + dependencies: + '@mapbox/geojson-rewind': 0.5.2 + '@mapbox/geojson-types': 1.0.2 + '@mapbox/jsonlint-lines-primitives': 2.0.2 + '@mapbox/mapbox-gl-supported': 1.5.0(mapbox-gl@1.13.3) + '@mapbox/point-geometry': 0.1.0 + '@mapbox/tiny-sdf': 1.2.5 + '@mapbox/unitbezier': 0.0.0 + '@mapbox/vector-tile': 1.3.1 + '@mapbox/whoots-js': 3.1.0 + csscolorparser: 1.0.3 + earcut: 2.2.4 + geojson-vt: 3.2.1 + gl-matrix: 3.4.4 + grid-index: 1.1.0 + murmurhash-js: 1.0.0 + pbf: 3.3.0 + potpack: 1.0.2 + quickselect: 2.0.0 + rw: 1.3.3 + supercluster: 7.1.5 + tinyqueue: 2.0.3 + vt-pbf: 3.1.3 + math-intrinsics@1.1.0: {} mathml-tag-names@4.0.0: {} @@ -8574,6 +9511,10 @@ snapshots: meow@14.1.0: {} + merge-refs@2.0.0(@types/react@19.2.14): + optionalDependencies: + '@types/react': 19.2.14 + merge2@1.4.1: {} meros@1.3.2(@types/node@24.12.2): @@ -8591,6 +9532,8 @@ snapshots: dependencies: mime-db: 1.52.0 + mimic-fn@1.2.0: {} + mimic-function@5.0.1: {} minimatch@10.2.5: @@ -8601,21 +9544,24 @@ snapshots: dependencies: brace-expansion: 1.1.13 - minimatch@9.0.3: + minimatch@5.1.9: dependencies: - brace-expansion: 2.0.3 + brace-expansion: 2.1.0 minimist@1.2.8: {} - mlly@1.8.2: + mkdirp@0.5.6: dependencies: - acorn: 8.16.0 - pathe: 2.0.3 - pkg-types: 1.3.1 - ufo: 1.6.3 + minimist: 1.2.8 + + moment@2.30.1: {} ms@2.1.3: {} + murmurhash-js@1.0.0: {} + + mute-stream@0.0.7: {} + mute-stream@2.0.0: {} nanoid@3.3.11: {} @@ -8652,6 +9598,11 @@ snapshots: normalize-path@3.0.0: {} + npm-run-path@6.0.0: + dependencies: + path-key: 4.0.0 + unicorn-magic: 0.3.0 + nwsapi@2.2.23: {} oauth-sign@0.9.0: {} @@ -8702,10 +9653,24 @@ snapshots: dependencies: wrappy: 1.0.2 + onetime@2.0.1: + dependencies: + mimic-fn: 1.2.0 + onetime@7.0.0: dependencies: mimic-function: 5.0.1 + openapi-typescript@7.13.0(typescript@6.0.2): + dependencies: + '@redocly/openapi-core': 1.34.14(supports-color@10.2.2) + ansi-colors: 4.1.3 + change-case: 5.4.4 + parse-json: 8.3.0 + supports-color: 10.2.2 + typescript: 6.0.2 + yargs-parser: 21.1.1 + option-t@20.3.1: {} optionator@0.8.3: @@ -8726,6 +9691,17 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 + ora@3.4.0: + dependencies: + chalk: 2.4.2 + cli-cursor: 2.1.0 + cli-spinners: 2.9.2 + log-symbols: 2.2.0 + strip-ansi: 5.2.0 + wcwidth: 1.0.1 + + os-tmpdir@1.0.2: {} + own-keys@1.0.1: dependencies: get-intrinsic: 1.3.0 @@ -8786,14 +9762,26 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 + p-locate@3.0.0: + dependencies: + p-limit: 2.3.0 + p-locate@5.0.0: dependencies: p-limit: 3.1.0 + p-try@2.2.0: {} + + papaparse@5.5.3: {} + param-case@3.0.4: dependencies: dot-case: 3.0.4 @@ -8816,6 +9804,12 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parse-json@8.3.0: + dependencies: + '@babel/code-frame': 7.29.0 + index-to-position: 1.2.0 + type-fest: 4.41.0 + parse-srcset@1.0.2: {} parse5@5.1.0: {} @@ -8830,12 +9824,16 @@ snapshots: dot-case: 3.0.4 tslib: 2.8.1 + path-exists@3.0.0: {} + path-exists@4.0.0: {} path-is-absolute@1.0.1: {} path-key@3.1.1: {} + path-key@4.0.0: {} + path-parse@1.0.7: {} path-root-regex@0.1.2: {} @@ -8846,10 +9844,17 @@ snapshots: path-type@4.0.0: {} - pathe@2.0.3: {} - pattern-key-compare@1.0.0: {} + pbf@3.3.0: + dependencies: + ieee754: 1.2.1 + resolve-protobuf-schema: 2.1.0 + + pdfjs-dist@5.4.296: + optionalDependencies: + '@napi-rs/canvas': 0.1.100 + performance-now@2.1.0: {} picocolors@0.2.1: {} @@ -8860,11 +9865,7 @@ snapshots: picomatch@4.0.4: {} - pkg-types@1.3.1: - dependencies: - confbox: 0.1.8 - mlly: 1.8.2 - pathe: 2.0.3 + pluralize@8.0.0: {} pn@1.1.0: {} @@ -9144,16 +10145,50 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + potpack@1.0.2: {} + + powerbi-client-react@2.0.2(react@19.2.4): + dependencies: + lodash.isequal: 4.5.0 + powerbi-client: 2.23.10 + react: 19.2.4 + + powerbi-client@2.23.10: + dependencies: + http-post-message: 0.3.0 + powerbi-models: 2.0.1 + powerbi-router: 0.1.5 + window-post-message-proxy: 0.3.0 + + powerbi-models@2.0.1: {} + + powerbi-router@0.1.5: + dependencies: + es6-promise: 3.3.1 + route-recognizer: 0.1.11 + prelude-ls@1.1.2: {} prelude-ls@1.2.1: {} + prettier@1.19.1: {} + + pretty-hrtime@1.0.3: {} + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 object-assign: 4.1.1 react-is: 16.13.1 + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + + protocol-buffers-schema@3.6.1: {} + psl@1.15.0: dependencies: punycode: 2.3.1 @@ -9166,8 +10201,12 @@ snapshots: qs@6.5.5: {} + quansync@1.0.0: {} + queue-microtask@1.2.3: {} + quickselect@2.0.0: {} + react-clientside-effect@1.2.8(react@19.2.4): dependencies: '@babel/runtime': 7.29.2 @@ -9213,6 +10252,21 @@ snapshots: react-is@16.13.1: {} + react-pdf@10.4.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + clsx: 2.1.1 + dequal: 2.0.3 + make-cancellable-promise: 2.0.0 + make-event-props: 2.0.0 + merge-refs: 2.0.0(@types/react@19.2.14) + pdfjs-dist: 5.4.296 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + tiny-invariant: 1.3.3 + warning: 4.0.3 + optionalDependencies: + '@types/react': 19.2.14 + react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.4): dependencies: react: 19.2.4 @@ -9250,6 +10304,8 @@ snapshots: react@19.2.4: {} + readdirp@4.1.2: {} + reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 @@ -9276,6 +10332,12 @@ snapshots: remove-trailing-spaces@1.0.9: {} + replace-in-file@3.4.4: + dependencies: + chalk: 2.4.2 + glob: 7.2.3 + yargs: 13.3.2 + request-promise-core@1.1.4(request@2.88.2): dependencies: lodash: 4.18.1 @@ -9315,12 +10377,18 @@ snapshots: require-from-string@2.0.2: {} + require-main-filename@2.0.0: {} + resolve-from@4.0.0: {} resolve-from@5.0.0: {} resolve-pkg-maps@1.0.0: {} + resolve-protobuf-schema@2.1.0: + dependencies: + protocol-buffers-schema: 3.6.1 + resolve.exports@1.1.1: {} resolve.imports@1.2.7: @@ -9342,19 +10410,22 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + restore-cursor@2.0.0: + dependencies: + onetime: 2.0.1 + signal-exit: 3.0.7 + restore-cursor@5.1.0: dependencies: onetime: 7.0.0 signal-exit: 4.1.0 + retry@0.12.0: {} + reusify@1.1.0: {} rfdc@1.4.1: {} - rimraf@3.0.2: - dependencies: - glob: 7.2.3 - rolldown@1.0.0-rc.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2): dependencies: '@oxc-project/types': 0.122.0 @@ -9379,10 +10450,20 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' + route-recognizer@0.1.11: {} + + run-async@2.4.1: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 + rw@1.3.3: {} + + rxjs@6.6.7: + dependencies: + tslib: 1.14.1 + safe-array-concat@1.1.3: dependencies: call-bind: 1.0.8 @@ -9433,6 +10514,8 @@ snapshots: tslib: 2.8.1 upper-case-first: 2.0.2 + set-blocking@2.0.0: {} + set-cookie-parser@2.7.2: {} set-function-length@1.2.2: @@ -9493,8 +10576,20 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + signal-exit@3.0.7: {} + signal-exit@4.1.0: {} + simple-git@3.36.0: + dependencies: + '@kwsites/file-exists': 1.1.1 + '@kwsites/promise-deferred': 1.1.1 + '@simple-git/args-pathspec': 1.0.3 + '@simple-git/argv-parser': 1.1.1 + debug: 4.4.3(supports-color@10.2.2) + transitivePeerDependencies: + - supports-color + slash@3.0.0: {} slash@5.1.0: {} @@ -9530,6 +10625,8 @@ snapshots: dependencies: tslib: 2.8.1 + sprintf-js@1.0.3: {} + sshpk@1.18.0: dependencies: asn1: 0.2.6 @@ -9544,8 +10641,6 @@ snapshots: stable-hash-x@0.2.0: {} - stable-hash@0.0.5: {} - stealthy-require@1.1.1: {} stop-iteration-iterator@1.1.0: @@ -9557,6 +10652,17 @@ snapshots: string-template@1.0.0: {} + string-width@2.1.1: + dependencies: + is-fullwidth-code-point: 2.0.0 + strip-ansi: 4.0.0 + + string-width@3.1.0: + dependencies: + emoji-regex: 7.0.3 + is-fullwidth-code-point: 2.0.0 + strip-ansi: 5.2.0 + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -9624,6 +10730,14 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + strip-ansi@4.0.0: + dependencies: + ansi-regex: 3.0.1 + + strip-ansi@5.2.0: + dependencies: + ansi-regex: 4.1.1 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -9638,6 +10752,8 @@ snapshots: strip-json-comments@5.0.3: {} + style-search@0.1.0: {} + stylelint-config-concentric@2.0.2(stylelint@17.6.0(typescript@6.0.2)): dependencies: stylelint: 17.6.0(typescript@6.0.2) @@ -9647,6 +10763,11 @@ snapshots: dependencies: stylelint: 17.6.0(typescript@6.0.2) + stylelint-config-standard@40.0.0(stylelint@17.6.0(typescript@6.0.2)): + dependencies: + stylelint: 17.6.0(typescript@6.0.2) + stylelint-config-recommended: 18.0.0(stylelint@17.6.0(typescript@6.0.2)) + stylelint-no-unused-selectors@1.0.40(stylelint@17.6.0(typescript@6.0.2)): dependencies: '@babel/parser': 7.29.2 @@ -9691,7 +10812,7 @@ snapshots: cosmiconfig: 9.0.1(typescript@6.0.2) css-functions-list: 3.3.3 css-tree: 3.2.1 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) fast-glob: 3.3.3 fastest-levenshtein: 1.0.16 file-entry-cache: 11.1.2 @@ -9720,8 +10841,16 @@ snapshots: - supports-color - typescript + supercluster@7.1.5: + dependencies: + kdbush: 3.0.0 + supports-color@10.2.2: {} + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -9733,6 +10862,8 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + svg-parser@2.0.4: {} + svg-tags@1.0.0: {} swap-case@2.0.2: @@ -9755,19 +10886,29 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 - text-table@0.2.0: {} + terminal-size@4.0.1: {} + + through@2.3.8: {} timeout-signal@2.0.0: {} + tiny-invariant@1.3.3: {} + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + tinyqueue@2.0.3: {} + title-case@3.0.3: dependencies: tslib: 2.8.1 + tmp@0.0.33: + dependencies: + os-tmpdir: 1.0.2 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -9787,20 +10928,12 @@ snapshots: dependencies: punycode: 2.3.1 - ts-api-utils@1.4.3(typescript@6.0.2): - dependencies: - typescript: 6.0.2 - ts-api-utils@2.5.0(typescript@6.0.2): dependencies: typescript: 6.0.2 ts-log@2.2.7: {} - tsconfck@3.1.6(typescript@6.0.2): - optionalDependencies: - typescript: 6.0.2 - tsconfig-paths@3.15.0: dependencies: '@types/json5': 0.0.29 @@ -9808,6 +10941,8 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 + tslib@1.14.1: {} + tslib@2.6.3: {} tslib@2.8.1: {} @@ -9826,7 +10961,7 @@ snapshots: dependencies: prelude-ls: 1.2.1 - type-fest@0.20.2: {} + type-fest@4.41.0: {} typed-array-buffer@1.0.3: dependencies: @@ -9876,8 +11011,6 @@ snapshots: typescript@6.0.2: {} - ufo@1.6.3: {} - unbash@2.2.0: {} unbox-primitive@1.1.0: @@ -9889,8 +11022,23 @@ snapshots: unc-path-regex@0.1.2: {} + unconfig-core@7.5.0: + dependencies: + '@quansync/fs': 1.0.0 + quansync: 1.0.0 + + unconfig@7.5.0: + dependencies: + '@quansync/fs': 1.0.0 + defu: 6.1.7 + jiti: 2.6.1 + quansync: 1.0.0 + unconfig-core: 7.5.0 + undici-types@7.16.0: {} + unicorn-magic@0.3.0: {} + unicorn-magic@0.4.0: {} universal-cookie@8.1.0: @@ -9939,6 +11087,8 @@ snapshots: dependencies: tslib: 2.8.1 + uri-js-replace@1.0.1: {} + uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -9976,13 +11126,33 @@ snapshots: core-util-is: 1.0.2 extsprintf: 1.3.0 - vite-tsconfig-paths@6.1.1(typescript@6.0.2)(vite@8.0.5(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.2)(jiti@2.6.1)(yaml@2.8.3)): + vite-plugin-checker@0.13.0(eslint@9.39.4(jiti@2.6.1))(meow@14.1.0)(optionator@0.9.4)(stylelint@17.6.0(typescript@6.0.2))(typescript@6.0.2)(vite@8.0.5(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.2)(jiti@2.6.1)(yaml@2.8.3)): + dependencies: + '@babel/code-frame': 7.29.0 + chokidar: 4.0.3 + npm-run-path: 6.0.0 + picocolors: 1.1.1 + picomatch: 4.0.4 + proper-lockfile: 4.1.2 + tiny-invariant: 1.3.3 + tinyglobby: 0.2.15 + vite: 8.0.5(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.2)(jiti@2.6.1)(yaml@2.8.3) + vscode-uri: 3.1.0 + optionalDependencies: + eslint: 9.39.4(jiti@2.6.1) + meow: 14.1.0 + optionator: 0.9.4 + stylelint: 17.6.0(typescript@6.0.2) + typescript: 6.0.2 + + vite-plugin-svgr@5.2.0(typescript@6.0.2)(vite@8.0.5(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.2)(jiti@2.6.1)(yaml@2.8.3)): dependencies: - debug: 4.4.3 - globrex: 0.1.2 - tsconfck: 3.1.6(typescript@6.0.2) + '@rollup/pluginutils': 5.3.0 + '@svgr/core': 8.1.0(typescript@6.0.2) + '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@6.0.2)) vite: 8.0.5(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.2)(jiti@2.6.1)(yaml@2.8.3) transitivePeerDependencies: + - rollup - supports-color - typescript @@ -10002,6 +11172,14 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' + vscode-uri@3.1.0: {} + + vt-pbf@3.1.3: + dependencies: + '@mapbox/point-geometry': 0.1.0 + '@mapbox/vector-tile': 1.3.1 + pbf: 3.3.0 + w3c-hr-time@1.0.2: dependencies: browser-process-hrtime: 1.0.0 @@ -10014,6 +11192,14 @@ snapshots: walk-up-path@4.0.0: {} + warning@4.0.3: + dependencies: + loose-envify: 1.4.0 + + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + web-streams-polyfill@3.3.3: {} webidl-conversions@4.0.2: {} @@ -10063,6 +11249,8 @@ snapshots: is-weakmap: 2.0.2 is-weakset: 2.0.4 + which-module@2.0.1: {} + which-typed-array@1.1.20: dependencies: available-typed-arrays: 1.0.7 @@ -10081,10 +11269,26 @@ snapshots: dependencies: isexe: 2.0.0 + window-post-message-proxy@0.3.0: + dependencies: + es6-promise: 3.3.1 + wonka@6.3.6: {} word-wrap@1.2.5: {} + wrap-ansi@10.0.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 8.2.0 + strip-ansi: 7.2.0 + + wrap-ansi@5.1.0: + dependencies: + ansi-styles: 3.2.1 + string-width: 3.1.0 + strip-ansi: 5.2.0 + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 @@ -10109,6 +11313,17 @@ snapshots: dependencies: signal-exit: 4.1.0 + write-yaml@1.0.0: + dependencies: + extend-shallow: 2.0.1 + js-yaml: 3.14.2 + write: 0.3.3 + + write@0.3.3: + dependencies: + fs-exists-sync: 0.1.0 + mkdirp: 0.5.6 + ws@7.5.10: {} ws@8.20.0: {} @@ -10117,14 +11332,36 @@ snapshots: xmlchars@2.2.0: {} + y18n@4.0.3: {} + y18n@5.0.8: {} yallist@3.1.1: {} + yaml-ast-parser@0.0.43: {} + yaml@2.8.3: {} + yargs-parser@13.1.2: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + yargs-parser@21.1.1: {} + yargs@13.3.2: + dependencies: + cliui: 5.0.0 + find-up: 3.0.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 3.1.0 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 13.1.2 + yargs@17.7.2: dependencies: cliui: 8.0.1 @@ -10139,14 +11376,8 @@ snapshots: yoctocolors-cjs@2.1.3: {} - zod-validation-error@3.5.4(zod@3.25.76): - dependencies: - zod: 3.25.76 - zod-validation-error@4.0.2(zod@4.3.6): dependencies: zod: 4.3.6 - zod@3.25.76: {} - zod@4.3.6: {} diff --git a/stylelint.config.js b/stylelint.config.js index 4b4e399..e35e118 100644 --- a/stylelint.config.js +++ b/stylelint.config.js @@ -12,13 +12,15 @@ const cssPaths = [ /** @type {import('stylelint').Config} */ const config = { extends: [ - 'stylelint-config-recommended', + 'stylelint-config-standard', 'stylelint-config-concentric', ], plugins: [ 'stylelint-value-no-unknown-custom-properties', + "@stylistic/stylelint-plugin" ], rules: { + '@stylistic/block-opening-brace-space-before': 'always', 'csstools/value-no-unknown-custom-properties': [ true, { diff --git a/tsconfig.app.json b/tsconfig.app.json index 67afcd4..04b8b70 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -6,7 +6,7 @@ "#generated/*": ["./generated/*"], "#views/*": ["app/views/*"], "#utils/*": ["app/utils/*"], - "#configs/*": ["app/configs/*"], + "#config": ["./app/config"], "#contexts/*": ["app/contexts/*"], "#components/*": ["app/components/*"], "#hooks/*": ["app/hooks/*"], @@ -21,7 +21,6 @@ "types": ["vite/client"], "skipLibCheck": true, - /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "verbatimModuleSyntax": true, @@ -29,11 +28,10 @@ "noEmit": true, "jsx": "react-jsx", - /* Linting */ "strict": true, - "noUnusedLocals": true, + "noUnusedLocals": false, "noUnusedParameters": true, - "erasableSyntaxOnly": true, + "erasableSyntaxOnly": false, "noFallthroughCasesInSwitch": true }, "include": ["app"] diff --git a/tsconfig.node.json b/tsconfig.node.json index d3c52ea..0fbee78 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -7,18 +7,16 @@ "types": ["node"], "skipLibCheck": true, - /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "verbatimModuleSyntax": true, "moduleDetection": "force", "noEmit": true, - /* Linting */ "noUnusedLocals": true, "noUnusedParameters": true, "erasableSyntaxOnly": true, "noFallthroughCasesInSwitch": true }, - "include": ["vite.config.ts"] + "include": ["vite.config.ts", "env.ts", "env.d.ts"] } diff --git a/vite.config.ts b/vite.config.ts index c1a993e..529b989 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,33 +1,71 @@ -import { defineConfig } from "vite"; -import react, { reactCompilerPreset } from '@vitejs/plugin-react' -import tsconfigPaths from "vite-tsconfig-paths"; -import babel from '@rolldown/plugin-babel' +import babel from '@rolldown/plugin-babel'; +import react, { reactCompilerPreset } from '@vitejs/plugin-react'; +import { execSync } from 'child_process'; +import { defineConfig } from 'vite'; +import checker from 'vite-plugin-checker'; +import svgr from 'vite-plugin-svgr'; +import { ValidateEnv as validateEnv } from '@togglecorp/vite-plugin-validate-env'; +/* Get commit hash */ +function getCommitHash(): string { + if (process.env.APP_COMMIT_HASH) { + return process.env.APP_COMMIT_HASH; + } + try { + return execSync('git rev-parse --short HEAD').toString().trim(); + } catch (error) { + throw new Error( + 'Unable to determine commit hash. You must either provide a commit hash using the APP_COMMIT_HASH environment variable,' + + " or provide a valid Git repository (submodule doesn't work with docker).", + ); + } +} +const commitHash = getCommitHash(); export default defineConfig(({ mode }) => { - const isProd = mode === "production"; - return { - plugins: [ - react(), - tsconfigPaths(), - babel({ presets: [reactCompilerPreset()] }) - ], - css: { - devSourcemap: isProd, - modules: { - scopeBehaviour: "local", - localsConvention: "camelCaseOnly", - }, - }, - sourcemap: isProd, - build: { - outDir: "build", - sourcemap: isProd, - }, - envPrefix: "APP_", - server: { - port: process.env.PORT ? parseInt(process.env.PORT, 10) : 3000, - strictPort: true, - }, - }; + const isProd = mode === 'production'; + return { + define: { + APP_COMMIT_HASH: JSON.stringify(commitHash), + }, + plugins: [ + isProd + ? checker({ + typescript: true, + eslint: { + lintCommand: 'eslint ./app', + }, + stylelint: { + lintCommand: 'stylelint "./app/**/*.css"', + }, + }) + : undefined, + svgr(), + validateEnv({ + configFile: 'env', + }), + react(), + babel({ presets: [reactCompilerPreset()] }), + ], + resolve: { + tsconfigPaths: true // Add this instead + }, + css: { + devSourcemap: isProd, + modules: { + scopeBehaviour: 'local', + localsConvention: 'camelCaseOnly', + }, + }, + sourcemap: isProd, + build: { + outDir: 'build', + sourcemap: isProd, + }, + envPrefix: 'APP_', + server: { + port: process.env.PORT ? parseInt(process.env.PORT, 10) : 3000, + strictPort: true, + }, + }; }); diff --git a/web-app-serve/.gitignore b/web-app-serve/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/web-app-serve/.gitignore @@ -0,0 +1 @@ +.env diff --git a/web-app-serve/docker-compose.yml b/web-app-serve/docker-compose.yml new file mode 100644 index 0000000..89ac405 --- /dev/null +++ b/web-app-serve/docker-compose.yml @@ -0,0 +1,12 @@ +name: ercs-eoc-serve + +services: + web-app-serve: + build: + context: ../ + target: web-app-serve + environment: + APPLY_CONFIG__ENABLE_DEBUG: true + env_file: .env + ports: + - '8050:80' \ No newline at end of file