diff --git a/.unimportedrc.json b/.unimportedrc.json index a8e45bbc..dbbfb582 100644 --- a/.unimportedrc.json +++ b/.unimportedrc.json @@ -4,6 +4,7 @@ "ignoreUnimported": ["**/*.d.ts", "**/*.test.*", "**/generated/**"], "ignoreUnused": [ "@apollo/client", + "@types/geojson", "@graphql-codegen/introspection", "@graphql-codegen/typescript-operations", "patch-package", diff --git a/Dockerfile b/Dockerfile index 96a0232b..d3b7e019 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,6 +18,7 @@ RUN git config --global --add safe.directory /code FROM dev AS builder COPY ./package.json ./pnpm-lock.yaml /code/ +COPY ./patches /code/patches/ # TODO: patches are not working with this? RUN pnpm install @@ -33,6 +34,7 @@ ENV APP_ENVIRONMENT=APP_ENVIRONMENT_PLACEHOLDER ENV APP_MAPBOX_ACCESS_TOKEN=APP_MAPBOX_ACCESS_TOKEN_PLACEHOLDER ENV APP_GOOGLE_ANALYTICS_ID=APP_GOOGLE_ANALYTICS_ID_PLACEHOLDER ENV APP_GRAPHQL_API_ENDPOINT=https://APP-GRAPHQL-API-ENDPOINT-PLACEHOLDER.COM/ +ENV APP_HCAPTCHA_SITEKEY=APP_HCAPTCHA_SITEKEY_PLACEHOLDER # Build variables (Requires backend pulled) ENV APP_GRAPHQL_CODEGEN_ENDPOINT=./backend/schema.graphql diff --git a/README.md b/README.md index e43815cf..422d95da 100644 --- a/README.md +++ b/README.md @@ -71,5 +71,8 @@ docker-compose up └── index.tsx (Defines root layout and requests fetched for DomainContext) ``` +## IFRC Alert Hub backend +The backend that serves the frontend application is maintained in a separate [repository](https://github.com/IFRCGo/alert-hub-backend). + ## External facing API Here is the documentation for [Alert Hub GraphQL Client Usage Guide](./APIDOCS.md) diff --git a/backend b/backend index 69450f82..a06277e3 160000 --- a/backend +++ b/backend @@ -1 +1 @@ -Subproject commit 69450f82253f675cf923779396f46cb2e809dcf8 +Subproject commit a06277e38cdc2922a9673decc6c83ca16837393e diff --git a/docker-compose-prod.yml b/docker-compose-prod.yml index ef4e7094..7af4362a 100644 --- a/docker-compose-prod.yml +++ b/docker-compose-prod.yml @@ -48,6 +48,9 @@ x-server: &base_server_setup # Static, Media configs DJANGO_STATIC_URL: ${DJANGO_STATIC_URL:-/dj-static/} DJANGO_MEDIA_URL: ${DJANGO_MEDIA_URL-/dj-media/} + # Misc + HCAPTCHA_SECRET: ${HCAPTCHA_SECRET?error} + HCAPTCHA_SITEKEY: ${HCAPTCHA_SITEKEY?error} volumes: - ./geographical_data/:/code/apps/cap_feed/geographical/ifrc-go-admin1-geojson/ - backend_data:/data/ @@ -98,6 +101,8 @@ services: APP_GRAPHQL_API_ENDPOINT: ${FRONTEND_APP_GRAPHQL_API_ENDPOINT?error} APP_MAPBOX_ACCESS_TOKEN: ${FRONTEND_APP_MAPBOX_ACCESS_TOKEN?error} APP_GRAPHQL_CODEGEN_ENDPOINT: ${FRONTEND_APP_GRAPHQL_CODEGEN_ENDPOINT?error} + env_file: + - .env command: | sh -c 'pnpm generate && pnpm build && rm -rf /client-build/* ; cp -r build/* /client-build/' volumes: diff --git a/docker-compose.yml b/docker-compose.yml index b9bda7c2..4ff82600 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -32,12 +32,16 @@ x-server: &base_server_setup # Redis config CELERY_BROKER_URL: ${CELERY_BROKER_URL:-redis://redis:6379/0} CACHE_REDIS_URL: ${CACHE_REDIS_URL:-redis://redis:6379/1} + TEST_CACHE_REDIS_URL: ${TEST_CACHE_REDIS_URL:-redis://redis:6379/11} # Email config EMAIL_HOST: ${EMAIL_HOST:-mailpit} EMAIL_PORT: ${EMAIL_PORT:-1025} EMAIL_HOST_USER: ${EMAIL_HOST_USER:-mailpit} EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD:-mailpit} DEFAULT_FROM_EMAIL: ${DEFAULT_FROM_EMAIL:-alert-hub-dev } + # Misc + HCAPTCHA_SECRET: ${HCAPTCHA_SECRET:-0x0000000000000000000000000000000000000000} + HCAPTCHA_SITEKEY: ${HCAPTCHA_SITEKEY:-10000000-ffff-ffff-ffff-000000000001} volumes: - ./backend/:/code - backend_data:/data/ diff --git a/env.ts b/env.ts index ed2f43ac..70782668 100644 --- a/env.ts +++ b/env.ts @@ -1,6 +1,5 @@ import { defineConfig, Schema } from '@julr/vite-plugin-validate-env'; -// TODO: Integrate .env for CI and remove optional() call on required fields export default defineConfig({ // Used in vite APP_GOOGLE_ANALYTICS_ID: Schema.string.optional(), @@ -23,6 +22,8 @@ export default defineConfig({ APP_GRAPHQL_API_ENDPOINT: Schema.string({ format: 'url', protocol: true, tld: false }), APP_MAPBOX_ACCESS_TOKEN: Schema.string(), + APP_HCAPTCHA_SITEKEY: Schema.string.optional(), + // Used in codegen APP_GRAPHQL_CODEGEN_ENDPOINT: Schema.string(), // NOTE: this is both url and file path diff --git a/nginx-serve/apply-config.sh b/nginx-serve/apply-config.sh index c81214e1..11eec813 100755 --- a/nginx-serve/apply-config.sh +++ b/nginx-serve/apply-config.sh @@ -30,6 +30,8 @@ find "$DESTINATION_DIRECTORY" -type f -exec sed -i "s|\|$APP_MAPBOX_ACCESS_TOKEN|g" {} + find "$DESTINATION_DIRECTORY" -type f -exec sed -i "s|\|$APP_GOOGLE_ANALYTICS_ID|g" {} + find "$DESTINATION_DIRECTORY" -type f -exec sed -i "s|\ { + className?: string; + name: N; +- label: string; ++ label: React.ReactNode; + variant?: ChipVariant; + onDelete?: (name: N, e: React.MouseEvent) => void; + } +diff --git a/package.json b/package.json +index 999e201527e87de45a377344ca40f13212549f61..8ce9c9c085e950405fb0d0f888d5c57876b5e080 100644 +--- a/package.json ++++ b/package.json +@@ -97,5 +97,10 @@ + "vite-plugin-lib-inject-css": "^1.3.0", + "vite-tsconfig-paths": "^4.2.3", + "vitest": "^1.1.1" ++ }, ++ "pnpm": { ++ "patchedDependencies": { ++ "@ifrc-go/ui@1.2.1": "patches/@ifrc-go__ui@1.2.1.patch" ++ } + } + } +diff --git a/patches/@ifrc-go__ui@1.2.1.patch b/patches/@ifrc-go__ui@1.2.1.patch +new file mode 100644 +index 0000000000000000000000000000000000000000..31603365255a6e1aa30c140c96b27fa3e93cd7fa +--- /dev/null ++++ b/patches/@ifrc-go__ui@1.2.1.patch +@@ -0,0 +1,13 @@ ++diff --git a/dist/components/Chip/index.d.ts b/dist/components/Chip/index.d.ts ++index 7345249eaad9a8fb959fe0330639292354032909..ceba3ed438c7fa0b687d07f7591bab624aff06bc 100644 ++--- a/dist/components/Chip/index.d.ts +++++ b/dist/components/Chip/index.d.ts ++@@ -2,7 +2,7 @@ export type ChipVariant = 'primary' | 'secondary' | 'tertiary'; ++ export interface Props { ++ className?: string; ++ name: N; ++- label: string; +++ label: React.ReactNode; ++ variant?: ChipVariant; ++ onDelete?: (name: N, e: React.MouseEvent) => void; ++ } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c4195d5e..da3f59fd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,22 +4,24 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +patchedDependencies: + '@ifrc-go/ui@1.2.1': + hash: c344e4kpykmuqcoldcef7qfjaq + path: patches/@ifrc-go__ui@1.2.1.patch + dependencies: '@apollo/client': specifier: ^3.9.9 version: 3.9.11(@types/react@18.2.79)(graphql@16.8.1)(react-dom@18.2.0)(react@18.2.0) - '@graphql-codegen/introspection': - specifier: ^4.0.3 - version: 4.0.3(graphql@16.8.1) - '@graphql-codegen/typescript-operations': - specifier: ^4.2.0 - version: 4.2.0(graphql@16.8.1) + '@hcaptcha/react-hcaptcha': + specifier: ^1.11.0 + version: 1.11.0(react-dom@18.2.0)(react@18.2.0) '@ifrc-go/icons': specifier: ^1.3.3 version: 1.3.3(react@18.2.0) '@ifrc-go/ui': - specifier: ^1.1.2 - version: 1.1.2(@ifrc-go/icons@1.3.3)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) + specifier: ^1.2.1 + version: 1.2.1(patch_hash=c344e4kpykmuqcoldcef7qfjaq)(@ifrc-go/icons@1.3.3)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0) '@mapbox/mapbox-gl-draw': specifier: ^1.4.3 version: 1.4.3 @@ -35,6 +37,9 @@ dependencies: '@togglecorp/re-map': specifier: ^0.2.0-beta-6 version: 0.2.0-beta-6(@mapbox/mapbox-gl-draw@1.4.3)(mapbox-gl@1.13.3)(react-dom@18.2.0)(react@18.2.0) + '@togglecorp/toggle-form': + specifier: ^2.0.4 + version: 2.0.4(react-dom@18.2.0)(react@18.2.0) '@turf/bbox': specifier: ^7.1.0 version: 7.1.0 @@ -47,9 +52,6 @@ dependencies: mapbox-gl: specifier: ^1.13.0 version: 1.13.3 - patch-package: - specifier: ^8.0.0 - version: 8.0.0 react: specifier: ^18.2.0 version: 18.2.0 @@ -70,6 +72,12 @@ devDependencies: '@graphql-codegen/client-preset': specifier: ^4.2.5 version: 4.2.5(graphql@16.8.1) + '@graphql-codegen/introspection': + specifier: ^4.0.3 + version: 4.0.3(graphql@16.8.1) + '@graphql-codegen/typescript-operations': + specifier: ^4.2.0 + version: 4.2.0(graphql@16.8.1) '@graphql-typed-document-node/core': specifier: ^3.2.0 version: 3.2.0(graphql@16.8.1) @@ -151,6 +159,9 @@ devDependencies: happy-dom: specifier: ^14.3.8 version: 14.7.1 + patch-package: + specifier: ^8.0.0 + version: 8.0.0 postcss: specifier: ^8.4.38 version: 8.4.38 @@ -231,6 +242,7 @@ packages: dependencies: '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 + dev: true /@antfu/utils@0.7.7: resolution: {integrity: sha512-gFPqTG7otEJ8uP6wrhDv6mqwGWYZKNvAcCq6u9hOj0c+IKCEsY4L1oC9trPq2SaWIzAfHvqfBDxF591JkMf+kg==} @@ -302,6 +314,7 @@ packages: transitivePeerDependencies: - encoding - supports-color + dev: true /@ardatan/sync-fetch@0.0.1: resolution: {integrity: sha512-xhlTqH0m31mnsG0tIP4ETgfSB6gXDaYYsUWTrlUV93fFQPI9dd8hE0Ot6MHLCtqgB32hwJAC3YZMWlXZw7AleA==} @@ -322,6 +335,7 @@ packages: /@babel/compat-data@7.24.4: resolution: {integrity: sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==} engines: {node: '>=6.9.0'} + dev: true /@babel/core@7.24.4: resolution: {integrity: sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg==} @@ -344,6 +358,7 @@ packages: semver: 6.3.1 transitivePeerDependencies: - supports-color + dev: true /@babel/generator@7.24.4: resolution: {integrity: sha512-Xd6+v6SnjWVx/nus+y0l1sxMOTOMBkyL4+BIdbALyatQnAe/SRVjANeDPSCYaX+i1iJmuGSKf3Z+E+V/va1Hvw==} @@ -353,12 +368,14 @@ packages: '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 jsesc: 2.5.2 + dev: true /@babel/helper-annotate-as-pure@7.22.5: resolution: {integrity: sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==} engines: {node: '>=6.9.0'} dependencies: '@babel/types': 7.24.0 + dev: true /@babel/helper-compilation-targets@7.23.6: resolution: {integrity: sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==} @@ -369,6 +386,7 @@ packages: browserslist: 4.23.0 lru-cache: 5.1.1 semver: 6.3.1 + dev: true /@babel/helper-create-class-features-plugin@7.24.4(@babel/core@7.24.4): resolution: {integrity: sha512-lG75yeuUSVu0pIcbhiYMXBXANHrpUPaOfu7ryAzskCgKUHuAxRQI5ssrtmF0X9UXldPlvT0XM/A4F44OXRt6iQ==} @@ -386,10 +404,12 @@ packages: '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 '@babel/helper-split-export-declaration': 7.22.6 semver: 6.3.1 + dev: true /@babel/helper-environment-visitor@7.22.20: resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==} engines: {node: '>=6.9.0'} + dev: true /@babel/helper-function-name@7.23.0: resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==} @@ -397,24 +417,28 @@ packages: dependencies: '@babel/template': 7.24.0 '@babel/types': 7.24.0 + dev: true /@babel/helper-hoist-variables@7.22.5: resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} engines: {node: '>=6.9.0'} dependencies: '@babel/types': 7.24.0 + dev: true /@babel/helper-member-expression-to-functions@7.23.0: resolution: {integrity: sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==} engines: {node: '>=6.9.0'} dependencies: '@babel/types': 7.24.0 + dev: true /@babel/helper-module-imports@7.24.3: resolution: {integrity: sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==} engines: {node: '>=6.9.0'} dependencies: '@babel/types': 7.24.0 + dev: true /@babel/helper-module-transforms@7.23.3(@babel/core@7.24.4): resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==} @@ -428,16 +452,19 @@ packages: '@babel/helper-simple-access': 7.22.5 '@babel/helper-split-export-declaration': 7.22.6 '@babel/helper-validator-identifier': 7.22.20 + dev: true /@babel/helper-optimise-call-expression@7.22.5: resolution: {integrity: sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==} engines: {node: '>=6.9.0'} dependencies: '@babel/types': 7.24.0 + dev: true /@babel/helper-plugin-utils@7.24.0: resolution: {integrity: sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==} engines: {node: '>=6.9.0'} + dev: true /@babel/helper-replace-supers@7.24.1(@babel/core@7.24.4): resolution: {integrity: sha512-QCR1UqC9BzG5vZl8BMicmZ28RuUBnHhAMddD8yHFHDRH9lLTZ9uUPehX8ctVPT8l0TKblJidqcgUUKGVrePleQ==} @@ -449,28 +476,33 @@ packages: '@babel/helper-environment-visitor': 7.22.20 '@babel/helper-member-expression-to-functions': 7.23.0 '@babel/helper-optimise-call-expression': 7.22.5 + dev: true /@babel/helper-simple-access@7.22.5: resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} engines: {node: '>=6.9.0'} dependencies: '@babel/types': 7.24.0 + dev: true /@babel/helper-skip-transparent-expression-wrappers@7.22.5: resolution: {integrity: sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==} engines: {node: '>=6.9.0'} dependencies: '@babel/types': 7.24.0 + dev: true /@babel/helper-split-export-declaration@7.22.6: resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} engines: {node: '>=6.9.0'} dependencies: '@babel/types': 7.24.0 + dev: true /@babel/helper-string-parser@7.24.1: resolution: {integrity: sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==} engines: {node: '>=6.9.0'} + dev: true /@babel/helper-validator-identifier@7.22.20: resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} @@ -479,6 +511,7 @@ packages: /@babel/helper-validator-option@7.23.5: resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==} engines: {node: '>=6.9.0'} + dev: true /@babel/helpers@7.24.4: resolution: {integrity: sha512-FewdlZbSiwaVGlgT1DPANDuCHaDMiOo+D/IDYRFYjHOuv66xMSJ7fQwwODwRNAPkADIO/z1EoF/l2BCWlWABDw==} @@ -489,6 +522,7 @@ packages: '@babel/types': 7.24.0 transitivePeerDependencies: - supports-color + dev: true /@babel/highlight@7.24.2: resolution: {integrity: sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==} @@ -505,6 +539,7 @@ packages: hasBin: true dependencies: '@babel/types': 7.24.0 + dev: true /@babel/plugin-proposal-class-properties@7.18.6(@babel/core@7.24.4): resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==} @@ -516,6 +551,7 @@ packages: '@babel/core': 7.24.4 '@babel/helper-create-class-features-plugin': 7.24.4(@babel/core@7.24.4) '@babel/helper-plugin-utils': 7.24.0 + dev: true /@babel/plugin-proposal-object-rest-spread@7.20.7(@babel/core@7.24.4): resolution: {integrity: sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==} @@ -530,6 +566,7 @@ packages: '@babel/helper-plugin-utils': 7.24.0 '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.4) '@babel/plugin-transform-parameters': 7.24.1(@babel/core@7.24.4) + dev: true /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.24.4): resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} @@ -538,6 +575,7 @@ packages: dependencies: '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.24.0 + dev: true /@babel/plugin-syntax-flow@7.24.1(@babel/core@7.24.4): resolution: {integrity: sha512-sxi2kLTI5DeW5vDtMUsk4mTPwvlUDbjOnoWayhynCwrw4QXRld4QEYwqzY8JmQXaJUtgUuCIurtSRH5sn4c7mA==} @@ -547,6 +585,7 @@ packages: dependencies: '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.24.0 + dev: true /@babel/plugin-syntax-import-assertions@7.24.1(@babel/core@7.24.4): resolution: {integrity: sha512-IuwnI5XnuF189t91XbxmXeCDz3qs6iDRO7GJ++wcfgeXNs/8FmIlKcpDSXNVyuLQxlwvskmI3Ct73wUODkJBlQ==} @@ -566,6 +605,7 @@ packages: dependencies: '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.24.0 + dev: true /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.24.4): resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} @@ -574,6 +614,7 @@ packages: dependencies: '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.24.0 + dev: true /@babel/plugin-transform-arrow-functions@7.24.1(@babel/core@7.24.4): resolution: {integrity: sha512-ngT/3NkRhsaep9ck9uj2Xhv9+xB1zShY3tM3g6om4xxCELwCDN4g4Aq5dRn48+0hasAql7s2hdBOysCfNpr4fw==} @@ -583,6 +624,7 @@ packages: dependencies: '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.24.0 + dev: true /@babel/plugin-transform-block-scoped-functions@7.24.1(@babel/core@7.24.4): resolution: {integrity: sha512-TWWC18OShZutrv9C6mye1xwtam+uNi2bnTOCBUd5sZxyHOiWbU6ztSROofIMrK84uweEZC219POICK/sTYwfgg==} @@ -592,6 +634,7 @@ packages: dependencies: '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.24.0 + dev: true /@babel/plugin-transform-block-scoping@7.24.4(@babel/core@7.24.4): resolution: {integrity: sha512-nIFUZIpGKDf9O9ttyRXpHFpKC+X3Y5mtshZONuEUYBomAKoM4y029Jr+uB1bHGPhNmK8YXHevDtKDOLmtRrp6g==} @@ -601,6 +644,7 @@ packages: dependencies: '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.24.0 + dev: true /@babel/plugin-transform-classes@7.24.1(@babel/core@7.24.4): resolution: {integrity: sha512-ZTIe3W7UejJd3/3R4p7ScyyOoafetUShSf4kCqV0O7F/RiHxVj/wRaRnQlrGwflvcehNA8M42HkAiEDYZu2F1Q==} @@ -617,6 +661,7 @@ packages: '@babel/helper-replace-supers': 7.24.1(@babel/core@7.24.4) '@babel/helper-split-export-declaration': 7.22.6 globals: 11.12.0 + dev: true /@babel/plugin-transform-computed-properties@7.24.1(@babel/core@7.24.4): resolution: {integrity: sha512-5pJGVIUfJpOS+pAqBQd+QMaTD2vCL/HcePooON6pDpHgRp4gNRmzyHTPIkXntwKsq3ayUFVfJaIKPw2pOkOcTw==} @@ -627,6 +672,7 @@ packages: '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.24.0 '@babel/template': 7.24.0 + dev: true /@babel/plugin-transform-destructuring@7.24.1(@babel/core@7.24.4): resolution: {integrity: sha512-ow8jciWqNxR3RYbSNVuF4U2Jx130nwnBnhRw6N6h1bOejNkABmcI5X5oz29K4alWX7vf1C+o6gtKXikzRKkVdw==} @@ -636,6 +682,7 @@ packages: dependencies: '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.24.0 + dev: true /@babel/plugin-transform-flow-strip-types@7.24.1(@babel/core@7.24.4): resolution: {integrity: sha512-iIYPIWt3dUmUKKE10s3W+jsQ3icFkw0JyRVyY1B7G4yK/nngAOHLVx8xlhA6b/Jzl/Y0nis8gjqhqKtRDQqHWQ==} @@ -646,6 +693,7 @@ packages: '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.24.0 '@babel/plugin-syntax-flow': 7.24.1(@babel/core@7.24.4) + dev: true /@babel/plugin-transform-for-of@7.24.1(@babel/core@7.24.4): resolution: {integrity: sha512-OxBdcnF04bpdQdR3i4giHZNZQn7cm8RQKcSwA17wAAqEELo1ZOwp5FFgeptWUQXFyT9kwHo10aqqauYkRZPCAg==} @@ -656,6 +704,7 @@ packages: '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.24.0 '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + dev: true /@babel/plugin-transform-function-name@7.24.1(@babel/core@7.24.4): resolution: {integrity: sha512-BXmDZpPlh7jwicKArQASrj8n22/w6iymRnvHYYd2zO30DbE277JO20/7yXJT3QxDPtiQiOxQBbZH4TpivNXIxA==} @@ -667,6 +716,7 @@ packages: '@babel/helper-compilation-targets': 7.23.6 '@babel/helper-function-name': 7.23.0 '@babel/helper-plugin-utils': 7.24.0 + dev: true /@babel/plugin-transform-literals@7.24.1(@babel/core@7.24.4): resolution: {integrity: sha512-zn9pwz8U7nCqOYIiBaOxoQOtYmMODXTJnkxG4AtX8fPmnCRYWBOHD0qcpwS9e2VDSp1zNJYpdnFMIKb8jmwu6g==} @@ -676,6 +726,7 @@ packages: dependencies: '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.24.0 + dev: true /@babel/plugin-transform-member-expression-literals@7.24.1(@babel/core@7.24.4): resolution: {integrity: sha512-4ojai0KysTWXzHseJKa1XPNXKRbuUrhkOPY4rEGeR+7ChlJVKxFa3H3Bz+7tWaGKgJAXUWKOGmltN+u9B3+CVg==} @@ -685,6 +736,7 @@ packages: dependencies: '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.24.0 + dev: true /@babel/plugin-transform-modules-commonjs@7.24.1(@babel/core@7.24.4): resolution: {integrity: sha512-szog8fFTUxBfw0b98gEWPaEqF42ZUD/T3bkynW/wtgx2p/XCP55WEsb+VosKceRSd6njipdZvNogqdtI4Q0chw==} @@ -696,6 +748,7 @@ packages: '@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.4) '@babel/helper-plugin-utils': 7.24.0 '@babel/helper-simple-access': 7.22.5 + dev: true /@babel/plugin-transform-object-super@7.24.1(@babel/core@7.24.4): resolution: {integrity: sha512-oKJqR3TeI5hSLRxudMjFQ9re9fBVUU0GICqM3J1mi8MqlhVr6hC/ZN4ttAyMuQR6EZZIY6h/exe5swqGNNIkWQ==} @@ -706,6 +759,7 @@ packages: '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.24.0 '@babel/helper-replace-supers': 7.24.1(@babel/core@7.24.4) + dev: true /@babel/plugin-transform-parameters@7.24.1(@babel/core@7.24.4): resolution: {integrity: sha512-8Jl6V24g+Uw5OGPeWNKrKqXPDw2YDjLc53ojwfMcKwlEoETKU9rU0mHUtcg9JntWI/QYzGAXNWEcVHZ+fR+XXg==} @@ -715,6 +769,7 @@ packages: dependencies: '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.24.0 + dev: true /@babel/plugin-transform-property-literals@7.24.1(@babel/core@7.24.4): resolution: {integrity: sha512-LetvD7CrHmEx0G442gOomRr66d7q8HzzGGr4PMHGr+5YIm6++Yke+jxj246rpvsbyhJwCLxcTn6zW1P1BSenqA==} @@ -724,6 +779,7 @@ packages: dependencies: '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.24.0 + dev: true /@babel/plugin-transform-react-display-name@7.24.1(@babel/core@7.24.4): resolution: {integrity: sha512-mvoQg2f9p2qlpDQRBC7M3c3XTr0k7cp/0+kFKKO/7Gtu0LSw16eKB+Fabe2bDT/UpsyasTBBkAnbdsLrkD5XMw==} @@ -733,6 +789,7 @@ packages: dependencies: '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.24.0 + dev: true /@babel/plugin-transform-react-jsx@7.23.4(@babel/core@7.24.4): resolution: {integrity: sha512-5xOpoPguCZCRbo/JeHlloSkTA8Bld1J/E1/kLfD1nsuiW1m8tduTA1ERCgIZokDflX/IBzKcqR3l7VlRgiIfHA==} @@ -746,6 +803,7 @@ packages: '@babel/helper-plugin-utils': 7.24.0 '@babel/plugin-syntax-jsx': 7.24.1(@babel/core@7.24.4) '@babel/types': 7.24.0 + dev: true /@babel/plugin-transform-shorthand-properties@7.24.1(@babel/core@7.24.4): resolution: {integrity: sha512-LyjVB1nsJ6gTTUKRjRWx9C1s9hE7dLfP/knKdrfeH9UPtAGjYGgxIbFfx7xyLIEWs7Xe1Gnf8EWiUqfjLhInZA==} @@ -755,6 +813,7 @@ packages: dependencies: '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.24.0 + dev: true /@babel/plugin-transform-spread@7.24.1(@babel/core@7.24.4): resolution: {integrity: sha512-KjmcIM+fxgY+KxPVbjelJC6hrH1CgtPmTvdXAfn3/a9CnWGSTY7nH4zm5+cjmWJybdcPSsD0++QssDsjcpe47g==} @@ -765,6 +824,7 @@ packages: '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.24.0 '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + dev: true /@babel/plugin-transform-template-literals@7.24.1(@babel/core@7.24.4): resolution: {integrity: sha512-WRkhROsNzriarqECASCNu/nojeXCDTE/F2HmRgOzi7NGvyfYGq1NEjKBK3ckLfRgGc6/lPAqP0vDOSw3YtG34g==} @@ -774,6 +834,7 @@ packages: dependencies: '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.24.0 + dev: true /@babel/runtime-corejs3@7.24.4: resolution: {integrity: sha512-VOQOexSilscN24VEY810G/PqtpFvx/z6UqDIjIWbDe2368HhDLkYN5TYwaEz/+eRCUkhJ2WaNLLmQAlxzfWj4w==} @@ -796,6 +857,7 @@ packages: '@babel/code-frame': 7.24.2 '@babel/parser': 7.24.4 '@babel/types': 7.24.0 + dev: true /@babel/traverse@7.24.1: resolution: {integrity: sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==} @@ -813,6 +875,7 @@ packages: globals: 11.12.0 transitivePeerDependencies: - supports-color + dev: true /@babel/types@7.24.0: resolution: {integrity: sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==} @@ -821,6 +884,7 @@ packages: '@babel/helper-string-parser': 7.24.1 '@babel/helper-validator-identifier': 7.22.20 to-fast-properties: 2.0.0 + dev: true /@changesets/apply-release-plan@7.0.0: resolution: {integrity: sha512-vfi69JR416qC9hWmFGSxj7N6wA5J222XNBmezSVATPWDVPIF7gkd4d8CpbEbXmRWbVrkoli3oerGS6dcL/BGsQ==} @@ -1841,7 +1905,7 @@ packages: transitivePeerDependencies: - encoding - supports-color - dev: false + dev: true /@graphql-codegen/plugin-helpers@5.0.3(graphql@16.8.1): resolution: {integrity: sha512-yZ1rpULIWKBZqCDlvGIJRSyj1B2utkEdGmXZTBT/GVayP4hyRYlkd36AJV/LfEsVD8dnsKL5rLz2VTYmRNlJ5Q==} @@ -1855,6 +1919,7 @@ packages: import-from: 4.0.0 lodash: 4.17.21 tslib: 2.6.2 + dev: true /@graphql-codegen/schema-ast@4.0.2(graphql@16.8.1): resolution: {integrity: sha512-5mVAOQQK3Oz7EtMl/l3vOQdc2aYClUzVDHHkMvZlunc+KlGgl81j8TLa+X7ANIllqU4fUEsQU3lJmk4hXP6K7Q==} @@ -1865,6 +1930,7 @@ packages: '@graphql-tools/utils': 10.1.3(graphql@16.8.1) graphql: 16.8.1 tslib: 2.6.2 + dev: true /@graphql-codegen/typed-document-node@5.0.6(graphql@16.8.1): resolution: {integrity: sha512-US0J95hOE2/W/h42w4oiY+DFKG7IetEN1mQMgXXeat1w6FAR5PlIz4JrRrEkiVfVetZ1g7K78SOwBD8/IJnDiA==} @@ -1896,6 +1962,7 @@ packages: transitivePeerDependencies: - encoding - supports-color + dev: true /@graphql-codegen/typescript@4.0.6(graphql@16.8.1): resolution: {integrity: sha512-IBG4N+Blv7KAL27bseruIoLTjORFCT3r+QYyMC3g11uY3/9TPpaUyjSdF70yBe5GIQ6dAgDU+ENUC1v7EPi0rw==} @@ -1911,6 +1978,7 @@ packages: transitivePeerDependencies: - encoding - supports-color + dev: true /@graphql-codegen/visitor-plugin-common@5.1.0(graphql@16.8.1): resolution: {integrity: sha512-eamQxtA9bjJqI2lU5eYoA1GbdMIRT2X8m8vhWYsVQVWD3qM7sx/IqJU0kx0J3Vd4/CSd36BzL6RKwksibytDIg==} @@ -1931,6 +1999,7 @@ packages: transitivePeerDependencies: - encoding - supports-color + dev: true /@graphql-tools/apollo-engine-loader@8.0.1(graphql@16.8.1): resolution: {integrity: sha512-NaPeVjtrfbPXcl+MLQCJLWtqe2/E4bbAqcauEOQ+3sizw1Fc2CNmhHRF8a6W4D0ekvTRRXAMptXYgA2uConbrA==} @@ -2195,6 +2264,7 @@ packages: dependencies: graphql: 16.8.1 tslib: 2.6.2 + dev: true /@graphql-tools/prisma-loader@8.0.3(@types/node@20.12.7)(graphql@16.8.1): resolution: {integrity: sha512-oZhxnMr3Jw2WAW1h9FIhF27xWzIB7bXWM8olz4W12oII4NiZl7VRkFw9IT50zME2Bqi9LGh9pkmMWkjvbOpl+Q==} @@ -2242,6 +2312,7 @@ packages: transitivePeerDependencies: - encoding - supports-color + dev: true /@graphql-tools/schema@10.0.3(graphql@16.8.1): resolution: {integrity: sha512-p28Oh9EcOna6i0yLaCFOnkcBDQECVf3SCexT6ktb86QNj9idnkhI+tCxnwZDh58Qvjd2nURdkbevvoZkvxzCog==} @@ -2294,6 +2365,7 @@ packages: dset: 3.1.3 graphql: 16.8.1 tslib: 2.6.2 + dev: true /@graphql-tools/wrap@10.0.5(graphql@16.8.1): resolution: {integrity: sha512-Cbr5aYjr3HkwdPvetZp1cpDWTGdD1Owgsb3z/ClzhmrboiK86EnQDxDvOJiQkDCPWE9lNBwj8Y4HfxroY0D9DQ==} @@ -2316,6 +2388,22 @@ packages: dependencies: graphql: 16.8.1 + /@hcaptcha/loader@1.2.4: + resolution: {integrity: sha512-3MNrIy/nWBfyVVvMPBKdKrX7BeadgiimW0AL/a/8TohNtJqxoySKgTJEXOQvYwlHemQpUzFrIsK74ody7JiMYw==} + dev: false + + /@hcaptcha/react-hcaptcha@1.11.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-UKHtzzVMHLTGwab5pgV96UbcXdyh5Qyq8E0G5DTyXq8txMvuDx7rSyC+BneOjWVW0a7O9VuZmkg/EznVLRE45g==} + peerDependencies: + react: '>= 16.3.0' + react-dom: '>= 16.3.0' + dependencies: + '@babel/runtime': 7.24.4 + '@hcaptcha/loader': 1.2.4 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@humanwhocodes/config-array@0.11.14: resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} engines: {node: '>=10.10.0'} @@ -2346,8 +2434,8 @@ packages: react: 18.2.0 dev: false - /@ifrc-go/ui@1.1.2(@ifrc-go/icons@1.3.3)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-BiKNoe72xrNkDSqf14dsu3w8f4h20Q2C91VWAX1ZWV64pGf6exZwxMGPQm6RPL299R6m47eTwHQndyQZJCjXoQ==} + /@ifrc-go/ui@1.2.1(patch_hash=c344e4kpykmuqcoldcef7qfjaq)(@ifrc-go/icons@1.3.3)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-ZdWffxnowjtY8wgIuheS4mf3P1JnoKKaEC8yrf0JHceb3fCenMXJqMBQEdRMaJNBtd4MhQhuls53uErzX/l2gA==} peerDependencies: '@ifrc-go/icons': ^1.3.1 react: ^18.2.0 @@ -2363,6 +2451,7 @@ packages: transitivePeerDependencies: - '@types/react' dev: false + patched: true /@jest/schemas@29.6.3: resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} @@ -2378,23 +2467,28 @@ packages: '@jridgewell/set-array': 1.2.1 '@jridgewell/sourcemap-codec': 1.4.15 '@jridgewell/trace-mapping': 0.3.25 + dev: true /@jridgewell/resolve-uri@3.1.2: resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} + dev: true /@jridgewell/set-array@1.2.1: resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} engines: {node: '>=6.0.0'} + dev: true /@jridgewell/sourcemap-codec@1.4.15: resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + dev: true /@jridgewell/trace-mapping@0.3.25: resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} dependencies: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.4.15 + dev: true /@jridgewell/trace-mapping@0.3.9: resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} @@ -3282,6 +3376,18 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@togglecorp/toggle-form@2.0.4(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-+EzRzXK/PKlisu44yARpxOkoeowz+0oKk2Rl3CdhxtBfTVfzG28aHAklDTubTBssS8hneGBTav2aInCqmwChfg==} + peerDependencies: + react: ^18.2.0 + react-dom: ^18.2.0 + dependencies: + '@babel/runtime-corejs3': 7.24.4 + '@togglecorp/fujs': 2.1.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@tsconfig/node10@1.0.11: resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} dev: true @@ -3770,7 +3876,7 @@ packages: /@yarnpkg/lockfile@1.1.0: resolution: {integrity: sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==} - dev: false + dev: true /abab@2.0.6: resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} @@ -4071,6 +4177,7 @@ packages: /asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + dev: true /asn1@0.2.6: resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} @@ -4112,11 +4219,12 @@ packages: /at-least-node@1.0.0: resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} engines: {node: '>= 4.0.0'} - dev: false + dev: true /auto-bind@4.0.0: resolution: {integrity: sha512-Hdw8qdNiqdJ8LqT0iK0sVzkFbzg6fhnQqqfWhBDxcHZvU75+B+ayzTy8x+k5Ix0Y92XOhOUlx74ps+bA6BeYMQ==} engines: {node: '>=8'} + dev: true /autoprefixer@10.4.19(postcss@8.4.38): resolution: {integrity: sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==} @@ -4420,6 +4528,7 @@ packages: /babel-plugin-syntax-trailing-function-commas@7.0.0-beta.0: resolution: {integrity: sha512-Xj9XuRuz3nTSbaTXWv3itLOcxyF4oPD8douBBmj7U9BBC6nEBYfyOJYQMf/8PJAFotC62UY5dFfIGEPr7WswzQ==} + dev: true /babel-plugin-transform-async-generator-functions@6.24.1: resolution: {integrity: sha512-uT7eovUxtXe8Q2ufcjRuJIOL0hg6VAUJhiWJBLxH/evYAw+aqoJLcYTR8hqx13iOx/FfbCMHgBmXWZjukbkyPg==} @@ -4778,6 +4887,7 @@ packages: '@babel/plugin-transform-spread': 7.24.1(@babel/core@7.24.4) '@babel/plugin-transform-template-literals': 7.24.1(@babel/core@7.24.4) babel-plugin-syntax-trailing-function-commas: 7.0.0-beta.0 + dev: true /babel-preset-stage-0@6.24.1: resolution: {integrity: sha512-MJD+xBbpsApbKlzAX0sOBF+VeFaUmv5s8FSOO7SSZpes1QgphCjq/UIGRFWSmQ/0i5bqQjLGCTXGGXqcLQ9JDA==} @@ -4887,6 +4997,7 @@ packages: /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true /balanced-match@2.0.0: resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==} @@ -4940,6 +5051,7 @@ packages: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 + dev: true /brace-expansion@2.0.1: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} @@ -4978,11 +5090,13 @@ packages: electron-to-chromium: 1.4.746 node-releases: 2.0.14 update-browserslist-db: 1.0.13(browserslist@4.23.0) + dev: true /bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} dependencies: node-int64: 0.4.0 + dev: true /buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -5027,6 +5141,7 @@ packages: dependencies: pascal-case: 3.1.2 tslib: 2.6.2 + dev: true /camelcase-keys@6.2.2: resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==} @@ -5058,6 +5173,7 @@ packages: /caniuse-lite@1.0.30001612: resolution: {integrity: sha512-lFgnZ07UhaCcsSZgWW0K5j4e69dK1u/ltrL9lTUiFOwNHs12S3UMIEYgBV0Z6C6hRDev7iRnMzzYmKabYdXF9g==} + dev: true /capital-case@1.0.4: resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} @@ -5065,6 +5181,7 @@ packages: no-case: 3.0.4 tslib: 2.6.2 upper-case-first: 2.0.2 + dev: true /caseless@0.12.0: resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} @@ -5122,6 +5239,7 @@ packages: title-case: 3.0.3 upper-case: 2.0.2 upper-case-first: 2.0.2 + dev: true /change-case@4.1.2: resolution: {integrity: sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==} @@ -5138,6 +5256,7 @@ packages: sentence-case: 3.0.4 snake-case: 3.0.4 tslib: 2.6.2 + dev: true /chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} @@ -5166,7 +5285,6 @@ packages: /ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} - dev: false /clean-css@5.3.3: resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==} @@ -5327,9 +5445,11 @@ packages: /common-tags@1.8.2: resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} engines: {node: '>=4.0.0'} + dev: true /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + dev: true /confbox@0.1.7: resolution: {integrity: sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==} @@ -5345,6 +5465,7 @@ packages: no-case: 3.0.4 tslib: 2.6.2 upper-case: 2.0.2 + dev: true /convert-source-map@1.9.0: resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} @@ -5352,6 +5473,7 @@ packages: /convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + dev: true /core-js-pure@3.37.0: resolution: {integrity: sha512-d3BrpyFr5eD4KcbRvQ3FTUx/KWmaDesr7+a3+1+P46IUnNoEt+oiLijPINZMEon7w9oGkIINWxrBAU9DEciwFQ==} @@ -5398,12 +5520,14 @@ packages: node-fetch: 2.7.0 transitivePeerDependencies: - encoding + dev: true /cross-inspect@1.0.0: resolution: {integrity: sha512-4PFfn4b5ZN6FMNGSZlyb7wUhuN8wvj8t/VQHZdM4JsDcruGJ8L2kf9zao98QIrBPFCpdk27qst/AGTl7pL3ypQ==} engines: {node: '>=16.0.0'} dependencies: tslib: 2.6.2 + dev: true /cross-spawn@5.1.0: resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} @@ -5419,6 +5543,7 @@ packages: path-key: 3.1.1 shebang-command: 2.0.0 which: 2.0.2 + dev: true /cross-var@1.1.0: resolution: {integrity: sha512-wIcFax9RNm5ayuORUeJ5MLxPbfh8XdZhhUpKutIszU46Fs9UIhEdPJ7+YguM+7FxEj+68hSQVyathVsIu84SiA==} @@ -5614,6 +5739,7 @@ packages: optional: true dependencies: ms: 2.1.2 + dev: true /decamelize-keys@1.1.1: resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} @@ -5685,6 +5811,7 @@ packages: /dependency-graph@0.11.0: resolution: {integrity: sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==} engines: {node: '>= 0.6.0'} + dev: true /dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} @@ -5781,6 +5908,7 @@ packages: dependencies: no-case: 3.0.4 tslib: 2.6.2 + dev: true /dotenv-cli@7.4.1: resolution: {integrity: sha512-fE1aywjRrWGxV3miaiUr3d2zC/VAiuzEGghi+QzgIA9fEf/M5hLMaRSXb4IxbUAwGmaLi0IozdZddnVU96acag==} @@ -5805,6 +5933,7 @@ packages: /dset@3.1.3: resolution: {integrity: sha512-20TuZZHCEZ2O71q9/+8BwKwZ0QtD9D8ObhrihJPr+vLLYlSuAU3/zL4cSlgbfeoGHTjCSJBa7NGcrF9/Bx/WJQ==} engines: {node: '>=4'} + dev: true /duplexify@3.7.1: resolution: {integrity: sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==} @@ -5828,6 +5957,7 @@ packages: /electron-to-chromium@1.4.746: resolution: {integrity: sha512-jeWaIta2rIG2FzHaYIhSuVWqC6KJYo7oSBX4Jv7g+aVujKztfvdpf+n6MGwZdC5hQXbax4nntykLH2juIQrfPg==} + dev: true /emoji-regex@10.3.0: resolution: {integrity: sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==} @@ -6496,9 +6626,11 @@ packages: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} dependencies: bser: 2.1.1 + dev: true /fbjs-css-vars@1.0.2: resolution: {integrity: sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==} + dev: true /fbjs@3.0.5: resolution: {integrity: sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==} @@ -6512,6 +6644,7 @@ packages: ua-parser-js: 1.0.37 transitivePeerDependencies: - encoding + dev: true /figures@2.0.0: resolution: {integrity: sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==} @@ -6572,7 +6705,7 @@ packages: resolution: {integrity: sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==} dependencies: micromatch: 4.0.5 - dev: false + dev: true /flat-cache@3.2.0: resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} @@ -6685,10 +6818,11 @@ packages: graceful-fs: 4.2.11 jsonfile: 6.1.0 universalify: 2.0.1 - dev: false + dev: true /fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + dev: true /fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} @@ -6727,6 +6861,7 @@ packages: /gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + dev: true /geojson-flatten@1.1.1: resolution: {integrity: sha512-k/6BCd0qAt7vdqdM1LkLfAy72EsLDy0laNwX0x2h49vfYCiQkRc4PSra8DNEdJ10EKRpwEvDXMb0dBknTJuWpQ==} @@ -6821,6 +6956,7 @@ packages: minimatch: 3.1.2 once: 1.4.0 path-is-absolute: 1.0.1 + dev: true /global-modules@2.0.0: resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==} @@ -6841,6 +6977,7 @@ packages: /globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} + dev: true /globals@13.24.0: resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} @@ -7059,6 +7196,7 @@ packages: dependencies: capital-case: 1.0.4 tslib: 2.6.2 + dev: true /hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} @@ -7162,6 +7300,7 @@ packages: /immutable@3.7.6: resolution: {integrity: sha512-AizQPcaofEtO11RZhPPHBOJRdo/20MKQF9mBLnVkBoyHi1/zXK8fzVdnEpSV9gxqtnh6Qomfp3F0xT5qP/vThw==} engines: {node: '>=0.8.0'} + dev: true /import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} @@ -7174,6 +7313,7 @@ packages: /import-from@4.0.0: resolution: {integrity: sha512-P9J71vT5nLlDeV8FHs5nNxaLbrpfAV5cF5srvbZfpwpcJoM/xZR3hiv+q+SAnuSmuGbXMWud063iIMx/V/EWZQ==} engines: {node: '>=12.2'} + dev: true /import-lazy@4.0.0: resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} @@ -7200,9 +7340,11 @@ packages: dependencies: once: 1.4.0 wrappy: 1.0.2 + dev: true /inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: true /ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} @@ -7272,6 +7414,7 @@ packages: dependencies: is-relative: 1.0.0 is-windows: 1.0.2 + dev: true /is-array-buffer@3.0.4: resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} @@ -7338,6 +7481,7 @@ packages: resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} engines: {node: '>=8'} hasBin: true + dev: true /is-domain@0.0.1: resolution: {integrity: sha512-hLm9uZUDm/sk0+xZgxyJluSf4B37sg3ivzv4ndTxNCAMnWFUUsHh1u4eh2maEcEvQl3mc65a9pJ/KURGItbLIg==} @@ -7406,6 +7550,7 @@ packages: resolution: {integrity: sha512-bVcMJy4X5Og6VZfdOZstSexlEy20Sr0k/p/b2IlQJlfdKAQuMpiv5w2Ccxb8sKdRUNAG1PnHVHjFSdRDVS6NlQ==} dependencies: tslib: 2.6.2 + dev: true /is-map@2.0.3: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} @@ -7451,6 +7596,7 @@ packages: engines: {node: '>=0.10.0'} dependencies: is-unc-path: 1.0.0 + dev: true /is-set@2.0.3: resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} @@ -7502,6 +7648,7 @@ packages: engines: {node: '>=0.10.0'} dependencies: unc-path-regex: 0.1.2 + dev: true /is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} @@ -7512,6 +7659,7 @@ packages: resolution: {integrity: sha512-44pxmxAvnnAOwBg4tHPnkfvgjPwbc5QIsSstNU+YcJ1ovxVzCWpSGosPJOZh/a1tdl81fbgnLc9LLv+x2ywbPQ==} dependencies: tslib: 2.6.2 + dev: true /is-weakmap@2.0.2: resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} @@ -7540,6 +7688,7 @@ packages: engines: {node: '>=8'} dependencies: is-docker: 2.2.1 + dev: true /isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} @@ -7666,6 +7815,7 @@ packages: resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} engines: {node: '>=4'} hasBin: true + dev: true /json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -7698,6 +7848,7 @@ packages: isarray: 2.0.5 jsonify: 0.0.1 object-keys: 1.1.1 + dev: true /json-stringify-safe@5.0.1: resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} @@ -7727,6 +7878,7 @@ packages: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} hasBin: true + dev: true /jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} @@ -7740,9 +7892,11 @@ packages: universalify: 2.0.1 optionalDependencies: graceful-fs: 4.2.11 + dev: true /jsonify@0.0.1: resolution: {integrity: sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==} + dev: true /jsprim@1.4.2: resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==} @@ -7782,7 +7936,7 @@ packages: resolution: {integrity: sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==} dependencies: graceful-fs: 4.2.11 - dev: false + dev: true /kleur@4.1.5: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} @@ -7905,6 +8059,7 @@ packages: /lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + dev: true /log-symbols@4.1.0: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} @@ -7951,11 +8106,13 @@ packages: resolution: {integrity: sha512-EVm/rR94FJTZi3zefZ82fLWab+GX14LJN4HrWBcuo6Evmsl9hEfnqxgcHCKb9q+mNf6EVdsjx/qucYFIIB84pg==} dependencies: tslib: 2.6.2 + dev: true /lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} dependencies: tslib: 2.6.2 + dev: true /lru-cache@4.1.5: resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} @@ -7967,6 +8124,7 @@ packages: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} dependencies: yallist: 3.1.1 + dev: true /lru-cache@6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} @@ -7987,6 +8145,7 @@ packages: /map-cache@0.2.2: resolution: {integrity: sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==} engines: {node: '>=0.10.0'} + dev: true /map-obj@1.0.1: resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} @@ -8129,6 +8288,7 @@ packages: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} dependencies: brace-expansion: 1.1.11 + dev: true /minimatch@4.2.3: resolution: {integrity: sha512-lIUdtK5hdofgCTu3aT0sOaHsYR37viUuIc0rwnnDXImbwFRcumyLMeZaM0t0I/fgxS6s6JMfu0rLD1Wz9pv1ng==} @@ -8197,6 +8357,7 @@ packages: /ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: true /ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -8232,6 +8393,7 @@ packages: dependencies: lower-case: 2.0.2 tslib: 2.6.2 + dev: true /node-addon-api@7.1.0: resolution: {integrity: sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==} @@ -8248,9 +8410,11 @@ packages: optional: true dependencies: whatwg-url: 5.0.0 + dev: true /node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + dev: true /node-modules-regexp@1.0.0: resolution: {integrity: sha512-JMaRS9L4wSRIR+6PTVEikTrq/lMGEZR43a48ETeilY0Q0iMwVnccMFrUM1k+tNzmYuIU0Vh710bCUqHX+/+ctQ==} @@ -8259,6 +8423,7 @@ packages: /node-releases@2.0.14: resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} + dev: true /normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} @@ -8311,6 +8476,7 @@ packages: /nullthrows@1.1.1: resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==} + dev: true /nwsapi@2.2.9: resolution: {integrity: sha512-2f3F0SEEer8bBu0dsNCFF50N0cTThV1nWFYcEYFZttdW0lDAoybv9cQoK7X7/68Z89S7FoRrVjP1LPX4XRf9vg==} @@ -8390,6 +8556,7 @@ packages: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: wrappy: 1.0.2 + dev: true /onetime@2.0.1: resolution: {integrity: sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==} @@ -8418,7 +8585,7 @@ packages: dependencies: is-docker: 2.2.1 is-wsl: 2.2.0 - dev: false + dev: true /open@8.4.2: resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} @@ -8557,6 +8724,7 @@ packages: dependencies: dot-case: 3.0.4 tslib: 2.6.2 + dev: true /parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} @@ -8572,6 +8740,7 @@ packages: is-absolute: 1.0.0 map-cache: 0.2.2 path-root: 0.1.1 + dev: true /parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} @@ -8595,6 +8764,7 @@ packages: dependencies: no-case: 3.0.4 tslib: 2.6.2 + dev: true /patch-package@8.0.0: resolution: {integrity: sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA==} @@ -8616,13 +8786,14 @@ packages: slash: 2.0.0 tmp: 0.0.33 yaml: 2.4.1 - dev: false + dev: true /path-case@3.0.4: resolution: {integrity: sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==} dependencies: dot-case: 3.0.4 tslib: 2.6.2 + dev: true /path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} @@ -8631,10 +8802,12 @@ packages: /path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} + dev: true /path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + dev: true /path-key@4.0.0: resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} @@ -8647,12 +8820,14 @@ packages: /path-root-regex@0.1.2: resolution: {integrity: sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==} engines: {node: '>=0.10.0'} + dev: true /path-root@0.1.1: resolution: {integrity: sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==} engines: {node: '>=0.10.0'} dependencies: path-root-regex: 0.1.2 + dev: true /path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} @@ -9206,6 +9381,7 @@ packages: resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==} dependencies: asap: 2.0.6 + dev: true /prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} @@ -9617,6 +9793,7 @@ packages: invariant: 2.2.4 transitivePeerDependencies: - encoding + dev: true /remedial@1.0.8: resolution: {integrity: sha512-/62tYiOe6DzS5BqVsNpH/nkGlX45C/Sp6V+NtiN6JQNS1Viay7cWkazmRkrQrdFj2eshDe96SIQNIoMxqhzBOg==} @@ -9789,6 +9966,7 @@ packages: hasBin: true dependencies: glob: 7.2.3 + dev: true /rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} @@ -9938,6 +10116,7 @@ packages: /semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true + dev: true /semver@7.6.0: resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==} @@ -9952,6 +10131,7 @@ packages: no-case: 3.0.4 tslib: 2.6.2 upper-case-first: 2.0.2 + dev: true /set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} @@ -9978,6 +10158,7 @@ packages: /setimmediate@1.0.5: resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + dev: true /shebang-command@1.2.0: resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} @@ -9990,6 +10171,7 @@ packages: engines: {node: '>=8'} dependencies: shebang-regex: 3.0.0 + dev: true /shebang-regex@1.0.0: resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} @@ -9998,6 +10180,7 @@ packages: /shebang-regex@3.0.0: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + dev: true /shell-quote@1.8.1: resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==} @@ -10026,6 +10209,7 @@ packages: /signedsource@1.0.0: resolution: {integrity: sha512-6+eerH9fEnNmi/hyM1DXcRK3pWdoMQtlkQ+ns0ntzunjKqp5i3sKCc80ym8Fib3iaYhdJUOPdhlJWj1tvge2Ww==} + dev: true /simple-git@3.24.0: resolution: {integrity: sha512-QqAKee9Twv+3k8IFOFfPB2hnk6as6Y6ACUpwCtQvRYBAes23Wv3SZlHVobAzqcE8gfsisCvPw3HGW3HYM+VYYw==} @@ -10045,7 +10229,7 @@ packages: /slash@2.0.0: resolution: {integrity: sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==} engines: {node: '>=6'} - dev: false + dev: true /slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} @@ -10103,6 +10287,7 @@ packages: dependencies: dot-case: 3.0.4 tslib: 2.6.2 + dev: true /source-map-js@1.2.0: resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} @@ -10164,6 +10349,7 @@ packages: resolution: {integrity: sha512-dblb9Et4DAtiZ5YSUZHLl4XhH4uK80GhAZrVXdN4O2P4gQ40Wa5UIOPUHlA/nFd2PLblBZWUioLMMAVrgpoYcA==} dependencies: tslib: 2.6.2 + dev: true /sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -10554,6 +10740,7 @@ packages: resolution: {integrity: sha512-kc6S2YS/2yXbtkSMunBtKdah4VFETZ8Oh6ONSmSd9bRxhqTrtARUCBUiWXH3xVPpvR7tz2CSnkuXVE42EcGnMw==} dependencies: tslib: 2.6.2 + dev: true /symbol-observable@4.0.0: resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} @@ -10646,6 +10833,7 @@ packages: resolution: {integrity: sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==} dependencies: tslib: 2.6.2 + dev: true /tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} @@ -10661,6 +10849,7 @@ packages: /to-fast-properties@2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} + dev: true /to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} @@ -10687,6 +10876,7 @@ packages: /tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + dev: true /tr46@1.0.1: resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} @@ -10929,6 +11119,7 @@ packages: /ua-parser-js@1.0.37: resolution: {integrity: sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==} + dev: true /ufo@1.5.3: resolution: {integrity: sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==} @@ -10945,6 +11136,7 @@ packages: /unc-path-regex@0.1.2: resolution: {integrity: sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==} engines: {node: '>=0.10.0'} + dev: true /unconfig@0.3.13: resolution: {integrity: sha512-N9Ph5NC4+sqtcOjPfHrRcHekBCadCXWTBzp2VYYbySOHW0PfD9XLCeXshTXjkPYwLrBr9AtSeU0CZmkYECJhng==} @@ -10994,6 +11186,7 @@ packages: /universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} + dev: true /unixify@1.0.0: resolution: {integrity: sha512-6bc58dPYhCMHHuwxldQxO3RRNZ4eCogZ/st++0+fcC1nr0jiGUtAdBJ2qzmLQWSxbtz42pWt4QQMiZ9HvZf5cg==} @@ -11011,16 +11204,19 @@ packages: browserslist: 4.23.0 escalade: 3.1.2 picocolors: 1.0.0 + dev: true /upper-case-first@2.0.2: resolution: {integrity: sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==} dependencies: tslib: 2.6.2 + dev: true /upper-case@2.0.2: resolution: {integrity: sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==} dependencies: tslib: 2.6.2 + dev: true /uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -11429,6 +11625,7 @@ packages: /webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + dev: true /webidl-conversions@4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} @@ -11463,6 +11660,7 @@ packages: dependencies: tr46: 0.0.3 webidl-conversions: 3.0.1 + dev: true /whatwg-url@7.1.0: resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} @@ -11542,6 +11740,7 @@ packages: hasBin: true dependencies: isexe: 2.0.0 + dev: true /why-is-node-running@2.2.2: resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==} @@ -11588,6 +11787,7 @@ packages: /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: true /write-file-atomic@5.0.1: resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} @@ -11647,6 +11847,7 @@ packages: /yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + dev: true /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} @@ -11659,6 +11860,7 @@ packages: resolution: {integrity: sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==} engines: {node: '>= 14'} hasBin: true + dev: true /yargs-parser@18.1.3: resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} diff --git a/src/App/Auth.tsx b/src/App/Auth.tsx new file mode 100644 index 00000000..0cbc1ee7 --- /dev/null +++ b/src/App/Auth.tsx @@ -0,0 +1,45 @@ +import { + Fragment, + type ReactElement, +} from 'react'; +import { Navigate } from 'react-router-dom'; + +import useAuth from '#hooks/domain/useAuth'; + +import { type ExtendedProps } from './routes/common'; + +interface Props { + children: ReactElement, + context: ExtendedProps, + absolutePath: string, +} +function Auth(props: Props) { + const { + context, + children, + absolutePath, + } = props; + + const { isAuthenticated } = useAuth(); + + if (context.visibility === 'is-authenticated' && !isAuthenticated) { + return ( + + ); + } + if (context.visibility === 'is-not-authenticated' && isAuthenticated) { + return ( + + ); + } + + return ( + + {children} + + ); +} + +export default Auth; diff --git a/src/App/index.tsx b/src/App/index.tsx index 8e16d45e..e703de75 100644 --- a/src/App/index.tsx +++ b/src/App/index.tsx @@ -10,6 +10,10 @@ import { createBrowserRouter, RouterProvider, } from 'react-router-dom'; +import { + gql, + useQuery, +} from '@apollo/client'; import { AlertContext, AlertContextProps, @@ -27,6 +31,11 @@ import mapboxgl from 'mapbox-gl'; import { mapboxToken } from '#config'; import RouteContext from '#contexts/route'; +import UserContext, { + UserAuth, + UserContextProps, +} from '#contexts/user'; +import { MeQuery } from '#generated/types/graphql'; import { KEY_LANGUAGE_STORAGE } from '#utils/constants'; import { getFromStorage, @@ -35,6 +44,25 @@ import { import wrappedRoutes, { unwrappedRoutes } from './routes'; +import styles from './styles.module.css'; + +const ME = gql` + query Me { + public { + me { + city + country + displayName + email + firstName + id + lastName + phoneNumber + } + } + } +`; + const router = createBrowserRouter(unwrappedRoutes); mapboxgl.accessToken = mapboxToken; mapboxgl.setRTLTextPlugin( @@ -66,13 +94,51 @@ function App() { }, []); const setAndStoreCurrentLanguage = useCallback( - (newLanugage: Language) => { - setCurrentLanguage(newLanugage); - setToStorage(KEY_LANGUAGE_STORAGE, newLanugage); + (newLanguage: Language) => { + setCurrentLanguage(newLanguage); + setToStorage(KEY_LANGUAGE_STORAGE, newLanguage); }, [], ); + // AUTH + + const [userAuth, setUserAuth] = useState(); + + const removeUserAuth = useCallback(() => { + setUserAuth(undefined); + }, []); + + // Hydration + useEffect(() => { + const language = getFromStorage(KEY_LANGUAGE_STORAGE); + setCurrentLanguage(language ?? 'en'); + }, []); + + const { + loading: meLoading, + } = useQuery( + ME, + { + onCompleted: (response) => { + if (response.public.me) { + setUserAuth(response.public.me); + } else { + removeUserAuth(); + } + }, + }, + ); + + const userContextValue = useMemo( + () => ({ + userAuth, + setUserAuth, + removeUserAuth, + }), + [userAuth, removeUserAuth], + ); + const registerLanguageNamespace = useCallback( (namespace: string, fallbackStrings: Record) => { setStrings( @@ -185,13 +251,24 @@ function App() { removeAlert, }), [alerts, addAlert, updateAlert, removeAlert]); + if (meLoading) { + return ( + // FIXME: Use translation +
+ Checking user session... +
+ ); + } + return ( - - - - - + + + + + + + ); } diff --git a/src/App/redirects/ActivationRedirect.tsx b/src/App/redirects/ActivationRedirect.tsx new file mode 100644 index 00000000..aaa7f7a0 --- /dev/null +++ b/src/App/redirects/ActivationRedirect.tsx @@ -0,0 +1,32 @@ +import { + generatePath, + useNavigate, + useParams, +} from 'react-router-dom'; + +import routes from '#routes'; + +interface ActivationParams { + userId: string | undefined; + token: string | undefined; + [key: string]: string | undefined; +} + +// eslint-disable-next-line import/prefer-default-export +export function Component() { + const { userId, token } = useParams(); + const navigate = useNavigate(); + + const activationLink = (userId && token && routes.activation.path) ? ({ + pathname: (generatePath(routes.activation.path, { userId, token })), + }) + : routes.pageNotFound.path; + + if (activationLink) { + navigate(activationLink); + } + + return null; +} + +Component.displayName = 'ActivationRedirect'; diff --git a/src/App/redirects/RecoverAccountRedirect.tsx b/src/App/redirects/RecoverAccountRedirect.tsx new file mode 100644 index 00000000..b7471d96 --- /dev/null +++ b/src/App/redirects/RecoverAccountRedirect.tsx @@ -0,0 +1,32 @@ +import { + generatePath, + useNavigate, + useParams, +} from 'react-router-dom'; + +import routes from '#routes'; + +interface ResetPasswordParams { + userId: string | undefined; + resetToken: string | undefined; + [key: string]: string | undefined; +} + +// eslint-disable-next-line import/prefer-default-export +export function Component() { + const { userId, resetToken } = useParams(); + const navigate = useNavigate(); + + const resetPasswordLink = (userId && resetToken && routes.recoverAccountConfirm.path) ? ({ + pathname: (generatePath(routes.recoverAccountConfirm.path, { userId, resetToken })), + }) + : routes.pageNotFound.path; + + if (resetPasswordLink) { + navigate(resetPasswordLink); + } + + return null; +} + +Component.displayName = 'RecoverAccountRedirect'; diff --git a/src/App/routes/common.tsx b/src/App/routes/common.tsx new file mode 100644 index 00000000..bfad45ea --- /dev/null +++ b/src/App/routes/common.tsx @@ -0,0 +1,47 @@ +import { + type MyInputIndexRouteObject, + type MyInputNonIndexRouteObject, + type MyOutputIndexRouteObject, + type MyOutputNonIndexRouteObject, + wrapRoute, +} from '#utils/routes'; +import { Component as RootLayout } from '#views/RootLayout'; + +import Auth from '../Auth'; +import PageError from '../PageError'; + +export type ExtendedProps = { + title: string, + visibility: 'is-authenticated' | 'is-not-authenticated' | 'anything', + permissions?: ( + params: Record | undefined | null, + ) => boolean; +}; + +interface CustomWrapRoute { + ( + myRouteOptions: MyInputIndexRouteObject + ): MyOutputIndexRouteObject + ( + myRouteOptions: MyInputNonIndexRouteObject + ): MyOutputNonIndexRouteObject +} + +export const customWrapRoute: CustomWrapRoute = wrapRoute; + +// NOTE: We should not use layout or index routes in links + +export const rootLayout = customWrapRoute({ + path: '/', + errorElement: , + component: { + eagerLoad: true, + render: RootLayout, + props: {}, + }, + wrapperComponent: Auth, + context: { + title: 'IFRC Alert Hub', + visibility: 'anything', + }, +}); diff --git a/src/App/routes/index.tsx b/src/App/routes/index.tsx index c6c6b483..e1f22676 100644 --- a/src/App/routes/index.tsx +++ b/src/App/routes/index.tsx @@ -6,11 +6,13 @@ import { MyOutputIndexRouteObject, MyOutputNonIndexRouteObject, unwrapRoute, - wrapRoute, } from '#utils/routes'; -import { Component as RootLayout } from '#views/RootLayout'; -import PageError from '../PageError'; +import Auth from '../Auth'; +import { + customWrapRoute, + rootLayout, +} from './common'; // NOTE: setting default ExtendedProps export type ExtendedProps = { @@ -27,33 +29,46 @@ export interface MyWrapRoute { ): MyOutputNonIndexRouteObject } -const customWrapRoute: MyWrapRoute = wrapRoute; - -const rootLayout = customWrapRoute({ - path: '/', - errorElement: , +type DefaultHomeChild = 'map'; +const homeLayout = customWrapRoute({ + parent: rootLayout, + forwardPath: 'map' satisfies DefaultHomeChild, component: { - render: RootLayout, - eagerLoad: true, + render: () => import('#views/Home'), props: {}, }, + wrapperComponent: Auth, context: { title: 'IFRC Alert Hub', visibility: 'anything', }, }); -type DefaultHomeChild = 'map'; -const homeLayout = customWrapRoute({ +const mySubscriptions = customWrapRoute({ parent: rootLayout, - forwardPath: 'map' satisfies DefaultHomeChild, + path: 'subscriptions', component: { - render: () => import('#views/Home'), + render: () => import('#views/MySubscriptions'), props: {}, }, + wrapperComponent: Auth, context: { - title: 'IFRC Alert Hub', - visibility: 'anything', + title: 'My Subscriptions', + visibility: 'is-authenticated', + }, +}); + +const subscriptionDetail = customWrapRoute({ + parent: rootLayout, + path: 'subscriptions/:subscriptionId', + component: { + render: () => import('#views/MySubscriptions/SubscriptionDetail'), + props: {}, + }, + wrapperComponent: Auth, + context: { + title: 'Subscription Detail', + visibility: 'is-authenticated', }, }); @@ -68,6 +83,7 @@ const homeIndex = customWrapRoute({ replace: true, }, }, + wrapperComponent: Auth, context: { title: 'IFRC Alert Hub', visibility: 'anything', @@ -81,6 +97,7 @@ const homeMap = customWrapRoute({ render: () => import('#views/Home/AlertsMap'), props: {}, }, + wrapperComponent: Auth, context: { title: 'IFRC Alert Hub - Map', visibility: 'anything', @@ -94,6 +111,7 @@ const homeTable = customWrapRoute({ render: () => import('#views/Home/AlertsTable'), props: {}, }, + wrapperComponent: Auth, context: { title: 'IFRC Alert Hub - Table', visibility: 'anything', @@ -107,12 +125,27 @@ const preferences = customWrapRoute({ render: () => import('#views/Preferences'), props: {}, }, + wrapperComponent: Auth, context: { title: 'Preferences', visibility: 'anything', }, }); +const historicalAlerts = customWrapRoute({ + parent: rootLayout, + path: 'historical-alerts', + component: { + render: () => import('#views/HistoricalAlerts'), + props: {}, + }, + wrapperComponent: Auth, + context: { + title: 'Historical Alerts', + visibility: 'anything', + }, +}); + const about = customWrapRoute({ parent: rootLayout, path: 'about', @@ -120,6 +153,7 @@ const about = customWrapRoute({ render: () => import('#views/About'), props: {}, }, + wrapperComponent: Auth, context: { title: 'About', visibility: 'anything', @@ -133,6 +167,7 @@ const resources = customWrapRoute({ render: () => import('#views/Resources'), props: {}, }, + wrapperComponent: Auth, context: { title: 'Resources', visibility: 'anything', @@ -146,6 +181,7 @@ const alertDetails = customWrapRoute({ render: () => import('#views/AlertDetails'), props: {}, }, + wrapperComponent: Auth, context: { title: 'Alert Details', visibility: 'anything', @@ -159,6 +195,7 @@ const allSourcesFeeds = customWrapRoute({ render: () => import('#views/AllSourcesFeeds'), props: {}, }, + wrapperComponent: Auth, context: { title: 'Sources Feeds', visibility: 'anything', @@ -172,11 +209,135 @@ const pageNotFound = customWrapRoute({ render: () => import('#views/PageNotFound'), props: {}, }, + wrapperComponent: Auth, context: { title: '404', visibility: 'anything', }, }); +const register = customWrapRoute({ + parent: rootLayout, + path: 'register', + component: { + render: () => import('#views/Register'), + props: {}, + }, + context: { + title: 'Register', + visibility: 'is-not-authenticated', + }, +}); + +const login = customWrapRoute({ + parent: rootLayout, + path: 'login', + component: { + render: () => import('#views/Login'), + props: {}, + }, + wrapperComponent: Auth, + context: { + title: 'Login', + visibility: 'is-not-authenticated', + }, +}); + +const recoverAccount = customWrapRoute({ + parent: rootLayout, + path: 'recover-account', + component: { + render: () => import('#views/RecoverAccount'), + props: {}, + }, + wrapperComponent: Auth, + context: { + title: 'Recover Account', + visibility: 'is-not-authenticated', + }, +}); + +/* const resendValidationEmail = customWrapRoute({ + parent: rootLayout, + path: 'resend-validation-email', + component: { + render: () => import('#views/ResendValidationEmail'), + props: {}, + }, + wrapperComponent: Auth, + context: { + title: 'Resend Validation Email', + visibility: 'is-not-authenticated', + }, +}); */ + +const cookiePolicy = customWrapRoute({ + parent: rootLayout, + path: 'cookie-policy', + component: { + render: () => import('#views/CookiePolicy'), + props: {}, + }, + wrapperComponent: Auth, + context: { + title: 'Cookie Policy', + visibility: 'anything', + }, +}); + +const recoverAccountConfirm = customWrapRoute({ + parent: rootLayout, + path: 'recover-account/:userId/:resetToken', + component: { + render: () => import('#views/RecoverAccountConfirm'), + props: {}, + }, + wrapperComponent: Auth, + context: { + title: 'Recover Account Confirm', + visibility: 'is-not-authenticated', + }, +}); + +const resetPasswordRedirect = customWrapRoute({ + parent: rootLayout, + path: 'permalink/user-password-reset/:userId/:resetToken', + component: { + render: () => import('../redirects/RecoverAccountRedirect.tsx'), + props: {}, + }, + wrapperComponent: Auth, + context: { + title: 'Reset Password Redirect', + visibility: 'is-not-authenticated', + }, +}); +const activation = customWrapRoute({ + parent: rootLayout, + path: 'activation/:userId/:token', + component: { + render: () => import('#views/Activation'), + props: {}, + }, + wrapperComponent: Auth, + context: { + title: 'Activation', + visibility: 'anything', + }, +}); + +const activationRedirect = customWrapRoute({ + parent: rootLayout, + path: 'permalink/user-activation/:userId/:token', + component: { + render: () => import('../redirects/ActivationRedirect'), + props: {}, + }, + wrapperComponent: Auth, + context: { + title: 'Activation Redirect', + visibility: 'anything', + }, +}); const wrappedRoutes = { rootLayout, @@ -190,6 +351,18 @@ const wrappedRoutes = { allSourcesFeeds, about, pageNotFound, + login, + recoverAccount, + // resendValidationEmail, + mySubscriptions, + cookiePolicy, + register, + historicalAlerts, + subscriptionDetail, + recoverAccountConfirm, + resetPasswordRedirect, + activationRedirect, + activation, }; export const unwrappedRoutes = unwrapRoute(Object.values(wrappedRoutes)); diff --git a/src/App/styles.module.css b/src/App/styles.module.css new file mode 100644 index 00000000..62cf9777 --- /dev/null +++ b/src/App/styles.module.css @@ -0,0 +1,7 @@ +.loading { + display: flex; + align-items: center; + justify-content: center; + width: 100vw; + height: 100vh; +} diff --git a/src/assets/icons/alerthub_Logo.png b/src/assets/icons/alerthub_Logo.png new file mode 100644 index 00000000..117d5628 Binary files /dev/null and b/src/assets/icons/alerthub_Logo.png differ diff --git a/src/assets/icons/alerthub_api.svg b/src/assets/icons/alerthub_api.svg new file mode 100644 index 00000000..1002f39b --- /dev/null +++ b/src/assets/icons/alerthub_api.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/components/Captcha/index.tsx b/src/components/Captcha/index.tsx new file mode 100644 index 00000000..f895d056 --- /dev/null +++ b/src/components/Captcha/index.tsx @@ -0,0 +1,85 @@ +import React, { useCallback } from 'react'; +import HCaptcha from '@hcaptcha/react-hcaptcha'; +import { + InputContainer, + InputContainerProps, +} from '@ifrc-go/ui'; + +import { hCaptchaKey } from '#config'; + +export type HCaptchaProps = Omit & { + name: T, + onChange: (value: string | undefined, name: T) => void; + elementRef?: React.RefObject; +}; + +function HCaptchaInput(props: HCaptchaProps) { + const { + actions, + actionsContainerClassName, + className, + disabled, + error, + errorContainerClassName, + hint, + hintContainerClassName, + icons, + iconsContainerClassName, + inputSectionClassName, + label, + readOnly, + name, + onChange, + elementRef, + } = props; + + const handleVerify = useCallback( + (token: string) => { + onChange(token, name); + }, + [onChange, name], + ); + const handleError = useCallback( + (err: string) => { + // eslint-disable-next-line no-console + console.error(err); + onChange(undefined, name); + }, + [onChange, name], + ); + const handleExpire = useCallback( + () => { + onChange(undefined, name); + }, + [onChange, name], + ); + + return ( + + )} + /> + ); +} + +export default HCaptchaInput; diff --git a/src/components/GlobalFooter/i18n.json b/src/components/GlobalFooter/i18n.json index d9d69632..3fabdd68 100644 --- a/src/components/GlobalFooter/i18n.json +++ b/src/components/GlobalFooter/i18n.json @@ -11,6 +11,8 @@ "footerContactUs":"Contact Us", "footerIFRC":"© IFRC Alert Hub {year} v{appVersion}", "globalFindOut": "Find Out More", - "globalHelpfulLinks": "Helpful links" + "globalHelpfulLinks": "Helpful links", + "policies": "Policies", + "cookiePolicy": "Cookie Policy" } } diff --git a/src/components/GlobalFooter/index.tsx b/src/components/GlobalFooter/index.tsx index c41b278c..70d21710 100644 --- a/src/components/GlobalFooter/index.tsx +++ b/src/components/GlobalFooter/index.tsx @@ -85,6 +85,18 @@ function GlobalFooter(props: Props) { +
+ + {strings.policies} + +
+ + {strings.cookiePolicy} + +
+
{strings.globalHelpfulLinks} diff --git a/src/components/Link/index.tsx b/src/components/Link/index.tsx index b85ac36a..1f26d4c5 100644 --- a/src/components/Link/index.tsx +++ b/src/components/Link/index.tsx @@ -21,7 +21,7 @@ import { } from '@togglecorp/fujs'; import RouteContext from '#contexts/route'; -import useAuth from '#hooks/useAuth'; +import useAuth from '#hooks/domain/useAuth'; import { type WrappedRoutes } from '../../App/routes'; diff --git a/src/components/Navbar/i18n.json b/src/components/Navbar/i18n.json index 78595b24..97ad5041 100644 --- a/src/components/Navbar/i18n.json +++ b/src/components/Navbar/i18n.json @@ -3,9 +3,13 @@ "strings": { "headerLogoAltText": "Alert Hub logo", "appLogin": "Login", + "appRegister":"Register", "appAbout": "About", "appResources": "Resources", "headerMenuHome": "Home", - "headerMenuMySubscription": "My Subscription" + "headerMenuMySubscription": "My Subscriptions", + "historicalAlerts": "Historical Alerts", + "logoutFailure": "Failed to logout", + "userLogout":"Logout" } } diff --git a/src/components/Navbar/index.tsx b/src/components/Navbar/index.tsx index e095a6af..36a2353e 100644 --- a/src/components/Navbar/index.tsx +++ b/src/components/Navbar/index.tsx @@ -1,3 +1,9 @@ +import { useContext } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { + gql, + useMutation, +} from '@apollo/client'; import { Button, Heading, @@ -10,18 +16,68 @@ import { _cs } from '@togglecorp/fujs'; import goLogo from '#assets/icons/go-logo-2020.svg'; import Link from '#components/Link'; import NavigationTab from '#components/NavigationTab'; +import UserContext from '#contexts/user'; +import { LogoutMutation } from '#generated/types/graphql'; +import useAuth from '#hooks/domain/useAuth'; +import useAlert from '#hooks/useAlert'; import LangaugeDropdown from './LanguageDropdown'; import i18n from './i18n.json'; import styles from './styles.module.css'; +const LOGOUT = gql` + mutation Logout { + private { + logout { + ok + errors + } + } + } +`; + interface Props { className?: string; } function Navbar(props: Props) { + const navigate = useNavigate(); const { className } = props; const strings = useTranslation(i18n); + const { isAuthenticated } = useAuth(); + const alert = useAlert(); + + const { + removeUserAuth: removeUser, + } = useContext(UserContext); + + const [ + triggerLogout, + { loading: logoutPending }, + ] = useMutation( + LOGOUT, + { + onCompleted: (logoutResponse) => { + const response = logoutResponse?.private?.logout; + if (response.ok) { + removeUser(); + navigate('/login'); + window.location.reload(); + } else { + alert.show( + strings.logoutFailure, + { variant: 'danger' }, + ); + } + }, + onError: () => { + alert.show( + strings.logoutFailure, + { variant: 'danger' }, + ); + }, + }, + ); return ( ); } + export default Navbar; diff --git a/src/components/Navbar/styles.module.css b/src/components/Navbar/styles.module.css index 7785e42f..f976e946 100644 --- a/src/components/Navbar/styles.module.css +++ b/src/components/Navbar/styles.module.css @@ -50,7 +50,6 @@ } .menu-item:hover { - text-decoration: underline; color: var(--go-ui-color-primary-red); } diff --git a/src/components/NonFiledError/i18n.json b/src/components/NonFiledError/i18n.json new file mode 100644 index 00000000..557f515b --- /dev/null +++ b/src/components/NonFiledError/i18n.json @@ -0,0 +1,6 @@ +{ + "namespace": "common", + "strings": { + "fallbackMessage":"Please correct all the errors before submission!" + } +} diff --git a/src/components/NonFiledError/index.tsx b/src/components/NonFiledError/index.tsx new file mode 100644 index 00000000..8602c693 --- /dev/null +++ b/src/components/NonFiledError/index.tsx @@ -0,0 +1,65 @@ +import { useMemo } from 'react'; +import { AlertLineIcon } from '@ifrc-go/icons'; +import { useTranslation } from '@ifrc-go/ui/hooks'; +import { + _cs, + isFalsyString, + isNotDefined, +} from '@togglecorp/fujs'; +import { + analyzeErrors, + Error, + getErrorObject, + nonFieldError, +} from '@togglecorp/toggle-form'; + +import i18n from './i18n.json'; +import styles from './styles.module.css'; + +export interface Props { + className?: string; + error?: Error; + withFallbackError?: boolean; +} + +function NonFieldError(props: Props) { + const { + className, + error, + withFallbackError, + } = props; + + const strings = useTranslation(i18n); + const errorObject = useMemo(() => getErrorObject(error), [error]); + + if (isNotDefined(errorObject)) { + return null; + } + + const hasError = analyzeErrors(errorObject); + if (!hasError) { + return null; + } + + const stringError = errorObject?.[nonFieldError] || ( + withFallbackError ? strings.fallbackMessage : undefined); + + if (isFalsyString(stringError)) { + return null; + } + + return ( +
+ +
+ {stringError} +
+
+ ); +} + +export default NonFieldError; diff --git a/src/components/NonFiledError/styles.module.css b/src/components/NonFiledError/styles.module.css new file mode 100644 index 00000000..b3e469f0 --- /dev/null +++ b/src/components/NonFiledError/styles.module.css @@ -0,0 +1,21 @@ +.non-field-error { + display: inline-flex; + gap: var(--go-ui-spacing-sm); + align-items: baseline; + animation: flash var(--go-ui-duration-animation-fast) ease-in-out; + animation-delay: var(--go-ui-duration-animation-slow); + color: var(--go-ui-color-red); + font-size: var(--go-ui-font-size-lg); + font-weight: var(--go-ui-font-weight-medium); + + .icon { + flex-shrink: 0; + /* font-size: var(--go-ui-height-icon-multiplier); */ + } +} + +@keyframes flash { + 0% { opacity: 1; } + 50% { opacity: 0.7; } + 100% { opacity: 1; } +} diff --git a/src/config.ts b/src/config.ts index 2d49ef79..17986041 100644 --- a/src/config.ts +++ b/src/config.ts @@ -5,11 +5,13 @@ const { APP_MAPBOX_ACCESS_TOKEN, APP_COMMIT_HASH, APP_VERSION, + APP_HCAPTCHA_SITEKEY, } = import.meta.env; export const appTitle = APP_TITLE; // not used export const environment = APP_ENVIRONMENT; // not used export const api = APP_GRAPHQL_API_ENDPOINT; export const mapboxToken = APP_MAPBOX_ACCESS_TOKEN; +export const hCaptchaKey = APP_HCAPTCHA_SITEKEY; export const appCommitHash = APP_COMMIT_HASH; export const appVersion = APP_VERSION; diff --git a/src/contexts/user.tsx b/src/contexts/user.tsx index c485f7fd..7908ab7c 100644 --- a/src/contexts/user.tsx +++ b/src/contexts/user.tsx @@ -1,21 +1,16 @@ import { createContext } from 'react'; export interface UserAuth { - id: number; - // FIXME: why do we not use displayName for other users? - displayName: string; - token: string; - expires: string; + displayName?: string | undefined | null; - username: string; - firstName: string | undefined; - lastName: string | undefined; + email: string | undefined; + firstName?: string | undefined | null; + lastName?: string | undefined | null; } export interface UserContextProps { userAuth: UserAuth | undefined, setUserAuth: (userDetails: UserAuth) => void, - hydrateUserAuth: () => void; removeUserAuth: () => void; } @@ -24,10 +19,6 @@ const UserContext = createContext({ // eslint-disable-next-line no-console console.warn('UserContext::setUser called without provider'); }, - hydrateUserAuth: () => { - // eslint-disable-next-line no-console - console.warn('UserContext::hydrateUser called without provider'); - }, removeUserAuth: () => { // eslint-disable-next-line no-console console.warn('UserContext::removeUser called without provider'); diff --git a/src/hooks/useAuth.ts b/src/hooks/domain/useAuth.ts similarity index 81% rename from src/hooks/useAuth.ts rename to src/hooks/domain/useAuth.ts index f74a7a38..0def19b9 100644 --- a/src/hooks/useAuth.ts +++ b/src/hooks/domain/useAuth.ts @@ -9,7 +9,7 @@ import UserContext from '#contexts/user'; function useAuth() { const { userAuth } = useContext(UserContext); - const isAuthenticated = isDefined(userAuth) && isDefined(userAuth.token); + const isAuthenticated = isDefined(userAuth); return useMemo( () => ({ isAuthenticated }), diff --git a/src/hooks/useFilterState.ts b/src/hooks/useFilterState.ts index 1d0be26d..b11ef193 100644 --- a/src/hooks/useFilterState.ts +++ b/src/hooks/useFilterState.ts @@ -6,11 +6,10 @@ import { } from 'react'; import { hasSomeDefinedValue } from '@ifrc-go/ui/utils'; import { isNotDefined } from '@togglecorp/fujs'; +import { EntriesAsList } from '@togglecorp/toggle-form'; import useDebouncedValue from '#hooks/useDebouncedValue'; -import { EntriesAsList } from '../types'; - type SortDirection = 'asc' | 'dsc'; interface SortParameter { name: string; @@ -135,6 +134,13 @@ function useFilterState(options: { [], ); + const resetFilter = useCallback( + () => { + dispatch({ type: 'reset-filter' }); + }, + [], + ); + const setFilterField = useCallback( (...args: EntriesAsList) => { const [val, key] = args; @@ -182,15 +188,22 @@ function useFilterState(options: { () => 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, diff --git a/src/index.tsx b/src/index.tsx index 17d4b25d..27be9297 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -25,6 +25,7 @@ const client = new ApolloClient({ headers: { 'Accept-Language': getFromStorage(KEY_LANGUAGE_STORAGE) ?? 'en' satisfies Language, }, + credentials: 'include', defaultOptions: { query: { fetchPolicy: 'network-only', diff --git a/src/utils/errorTransform.ts b/src/utils/errorTransform.ts new file mode 100644 index 00000000..a8a15385 --- /dev/null +++ b/src/utils/errorTransform.ts @@ -0,0 +1,86 @@ +import { + isDefined, + isNotDefined, + listToMap, +} from '@togglecorp/fujs'; +import { nonFieldError } from '@togglecorp/toggle-form'; + +export interface Error { + [ nonFieldError]?: string | undefined; + [key: string]: string | Error | undefined; +} + +export interface ObjectError { + // clientId is sent by the server for bulk updates + clientId: string | undefined; + + field: string; + messages?: string; + objectErrors?: ObjectError[]; + arrayErrors?: (ArrayError | null)[]; +} + +interface ArrayError { + clientId: string; + messages?: string; + objectErrors?: ObjectError[]; +} + +function transformObject(errors: ObjectError[] | undefined): Error | undefined { + if (isNotDefined(errors)) { + return undefined; + } + + const topLevelError = errors.find((error) => error.field === 'nonFieldErrors'); + const finalNonFieldErrors = topLevelError?.messages; + + const fieldErrors = errors.filter((error) => error.field !== 'nonFieldErrors'); + const finalFieldErrors: Error = listToMap( + fieldErrors, + (error) => error.field, + (error) => { + if (isDefined(error.messages)) { + return error.messages; + } + const objectErrors = isDefined(error.objectErrors) + ? transformObject(error.objectErrors) + : undefined; + + const arrayErrors = isDefined(isDefined(error.arrayErrors)) + // eslint-disable-next-line @typescript-eslint/no-use-before-define + ? transformArray(error.arrayErrors) + : undefined; + + if (!objectErrors && !arrayErrors) { + return undefined; + } + return { ...objectErrors, ...arrayErrors }; + }, + ); + + return { + [nonFieldError]: finalNonFieldErrors, + ...finalFieldErrors, + }; +} + +function transformArray(errors: (ArrayError | null)[] | undefined): Error | undefined { + if (isNotDefined(errors)) { + return undefined; + } + const filteredErrors = errors.filter(isDefined); + + const topLevelError = filteredErrors.find((error) => error.clientId === 'nonMemberErrors'); + const memberErrors = filteredErrors.filter((error) => error.clientId !== 'nonMemberErrors'); + + return { + [nonFieldError]: topLevelError?.messages, + ...listToMap( + memberErrors, + (error) => error.clientId, + (error) => transformObject(error.objectErrors), + ), + }; +} + +export const transformToFormError = transformObject; diff --git a/src/views/Activation/i18n.json b/src/views/Activation/i18n.json new file mode 100644 index 00000000..c09a861b --- /dev/null +++ b/src/views/Activation/i18n.json @@ -0,0 +1,9 @@ +{ + "namespace": "activation", + "strings": { + "activationSuccessMessage":"Your account has been successfully activated!", + "goToLogin":"Go to Login", + "activationFailMessage":"An error occurred during activation. Please try again." + } +} + diff --git a/src/views/Activation/index.tsx b/src/views/Activation/index.tsx new file mode 100644 index 00000000..b6b63521 --- /dev/null +++ b/src/views/Activation/index.tsx @@ -0,0 +1,103 @@ +import { + useEffect, + useState, +} from 'react'; +import { useParams } from 'react-router-dom'; +import { + gql, + useMutation, +} from '@apollo/client'; +import { Message } from '@ifrc-go/ui'; +import { useTranslation } from '@ifrc-go/ui/hooks'; + +import Link from '#components/Link'; +import Page from '#components/Page'; +import useAlert from '#hooks/useAlert'; + +import i18n from './i18n.json'; +import styles from './styles.module.css'; + +const ACCOUNT_ACTIVATION_MUTATION = gql` + mutation AccountActivation($data: UserActivationInput!) { + public { + accountActivation(data: $data) { + errors + ok + } + } + } +`; + +// eslint-disable-next-line import/prefer-default-export +export function Component() { + const { userId, token } = useParams<{ userId?: string, token?: string }>(); + const alert = useAlert(); + const strings = useTranslation(i18n); + const [isErrored, setIsError] = useState(false); + const [isSubmitted, setIsSubmitted] = useState(false); + const [ + activate, + ] = useMutation(ACCOUNT_ACTIVATION_MUTATION, { + onCompleted: (response) => { + const activateRes = response?.public?.accountActivation; + if (!response) { + return; + } + if (activateRes.ok) { + setIsSubmitted(true); + } else { + setIsError(true); + } + }, + onError: () => { + alert.show( + strings.activationFailMessage, + { variant: 'danger' }, + ); + }, + }); + + useEffect(() => { + if (userId && token) { + activate({ + variables: { + data: { + uuid: userId, + token, + }, + }, + }); + } + }, [token, activate, userId]); + + if (isSubmitted) { + return ( + + +
+ + {strings.goToLogin} + +
+ +
+ ); + } + if (isErrored) { + return ( + + + + ); + } + return null; +} + +Component.displayName = 'Activation'; diff --git a/src/views/Activation/styles.module.css b/src/views/Activation/styles.module.css new file mode 100644 index 00000000..2b4e1630 --- /dev/null +++ b/src/views/Activation/styles.module.css @@ -0,0 +1,8 @@ +.activation { + display: flex; + flex-direction: column; + gap: var(--go-ui-spacing-xs); + align-items: center; + text-align: center; + +} \ No newline at end of file diff --git a/src/views/AlertDetails/AlertInfo/AreaInfoDetail/index.tsx b/src/views/AlertDetails/AlertInfo/AreaInfoDetail/index.tsx index c6074fca..0c30eb1d 100644 --- a/src/views/AlertDetails/AlertInfo/AreaInfoDetail/index.tsx +++ b/src/views/AlertDetails/AlertInfo/AreaInfoDetail/index.tsx @@ -236,7 +236,6 @@ function AreaInfoDetail(props: Props) { onChange={setSelectedFeature} /> )} - withGridViewInFilter headingLevel={4} >
diff --git a/src/views/AllSourcesFeeds/index.tsx b/src/views/AllSourcesFeeds/index.tsx index f80cb43b..b10ca554 100644 --- a/src/views/AllSourcesFeeds/index.tsx +++ b/src/views/AllSourcesFeeds/index.tsx @@ -117,7 +117,6 @@ export function Component() { heading={strings.sourceFeedsTitle} > ('disclaimer'); + + const disclaimerRef = useRef(null); + const useOfOurInformationRef = useRef(null); + const ourPrivacyPolicyRef = useRef(null); + + const handleTabChange = (newTab: TitlesOptionKey) => { + setActiveTitleOption(newTab); + + const tabRefs = { + disclaimer: disclaimerRef, + 'use-of-our-information': useOfOurInformationRef, + 'our-privacy-policy': ourPrivacyPolicyRef, + }; + tabRefs[newTab]?.current?.scrollIntoView({ behavior: 'smooth' }); + }; + + return ( + +
+ + + + {strings.disclaimerTitle} + + + {strings.useOfOurInformationTitle} + + + {strings.ourPrivacyPolicyHeading} + + + +
+ + +
{strings.useOfOurInformationDescription1}
+
+ { resolveToComponent( + strings.useOfOurInformationDescription2, + { + termsLink: ( + + {strings.useOfOurInformationAudiovisualLink} + + ), + }, + )} +
+
{strings.useOfOurInformationDescription3}
+
+ + {strings.useOfOurInformationDescriptionLink} + +
+ + )} + withHeaderBorder + withInternalPadding + /> + +
+ {resolveToString(strings.ourPrivacyPolicyContent, { + publishedDay: 'November', + publishedDate: 29, + publishedYear: 2021, + })} +
+ + +
{strings.informationProvideDescription1}
+
{strings.informationProvideDescription2}
+ + )} + /> + +
{strings.automaticallyCollectedDescription}
+
    +
  • {strings.automaticallyCollectedList1}
  • +
  • {strings.automaticallyCollectedList2}
  • +
  • {strings.automaticallyCollectedList3}
  • +
  • {strings.automaticallyCollectedList4}
  • +
  • {strings.automaticallyCollectedList5}
  • +
+ + )} + /> + +
{strings.ifrcLimitedCookiesAnalyticDescription}
+
    +
  • {strings.ifrcLimitedCookiesAnalyticList1}
  • +
  • {strings.ifrcLimitedCookiesAnalyticList2}
  • +
  • {strings.ifrcLimitedCookiesAnalyticList3}
  • +
  • {strings.ifrcLimitedCookiesAnalyticList4}
  • +
+
{strings.ifrcLimitedCookiesAnalyticDescription2}
+
{strings.ifrcLimitedCookiesAnalyticDescription3}
+ + )} + /> +
+ +
{strings.howInformationUsedDescription}
+
    +
  • {strings.howInformationUsedDescriptionList1}
  • +
  • {strings.howInformationUsedDescriptionList2}
  • +
  • {strings.howInformationUsedDescriptionList3}
  • +
  • {strings.howInformationUsedDescriptionList4}
  • +
  • {strings.howInformationUsedDescriptionList5}
  • +
  • {strings.howInformationUsedDescriptionList6}
  • +
+ + )} + /> + +
{strings.dataAccessSharingDescription1}
+
{strings.dataAccessSharingDescription2}
+
{strings.dataAccessSharingDescription3}
+ + )} + /> + +
{strings.storageSecurityQuestionsDataDescription1}
+
{strings.storageSecurityQuestionsDataDescription2}
+
+ { resolveToComponent( + strings.storageSecurityQuestionsAboutDataDescription3, + { + termsLink: ( + + {strings.policyProtectionOfPersonalDataLink} + + ), + }, + )} +
+
+ {resolveToComponent( + strings.storageSecurityQuestionsAboutDataDescription4, + { + termsLink: ( + + {strings.dataProtectionPageLink} + + ), + }, + )} +
+
{strings.storageSecurityQuestionsDataDescription5}
+
+ + { resolveToComponent( + strings.storageSecurityQuestionsDataGoEnquires, + { + termsLink: ( + + im@ifrc.org + + ), + }, + )} + +
+
+ + { resolveToComponent( + strings.storageSecurityQuestionsDataDonations, + { + termsLink: ( + + prd@ifrc.org + + ), + }, + )} + +
+
+ + {resolveToComponent( + strings.storageSecurityQuestionsDataRecruitment, + { + termsLink: ( + + ask.hr@ifrc.org + + ), + }, + )} + +
+
+ + { resolveToComponent( + strings.securityQuestionsWebpageCollection, + { + termsLink: ( + + webteam@ifrc.org + + ), + }, + )} + +
+
+ + { resolveToComponent( + strings.storageSecurityQuestionsDataEnquires, + { + termsLink: ( + + dataprotection@ifrc.org + + ), + }, + )} + +
+ + )} + /> + + {strings.privilegesAndImmunitiesDescription} +
+ )} + /> + +
{strings.noteOnLinksToExternalWebsitesDescription1}
+
{strings.noteOnLinksToExternalWebsitesDescription2}
+ + )} + /> +
+
+
+ + ); +} + +Component.displayName = 'CookiePolicy'; diff --git a/src/views/CookiePolicy/styles.module.css b/src/views/CookiePolicy/styles.module.css new file mode 100644 index 00000000..ce73fa2c --- /dev/null +++ b/src/views/CookiePolicy/styles.module.css @@ -0,0 +1,36 @@ +.cookie-page { + display: flex; + gap: var(--go-ui-spacing-xl); + + .side-titles { + display: flex; + flex-direction: column; + flex-shrink: 0; + gap: var(--go-ui-spacing-md); + padding: var(--go-ui-spacing-sm); + } + + .header-description { + display: flex; + flex-direction: column; + gap: var(--go-ui-spacing-md); + } + + .main-content { + display: flex; + flex-direction: column; + gap: var(--go-ui-spacing-md); + + .first-level-content { + display: flex; + flex-direction: column; + gap: var(--go-ui-spacing-md); + + .second-level-content { + display: flex; + flex-direction: column; + gap: var(--go-ui-spacing-sm); + } + } + } +} diff --git a/src/views/HistoricalAlerts/AlertActions/i18n.json b/src/views/HistoricalAlerts/AlertActions/i18n.json new file mode 100644 index 00000000..28dcdf39 --- /dev/null +++ b/src/views/HistoricalAlerts/AlertActions/i18n.json @@ -0,0 +1,6 @@ +{ + "namespace": "alertActions", + "strings": { + "alertTableViewDetailsTitle": "View Details" + } +} \ No newline at end of file diff --git a/src/views/HistoricalAlerts/AlertActions/index.tsx b/src/views/HistoricalAlerts/AlertActions/index.tsx new file mode 100644 index 00000000..aa20e83a --- /dev/null +++ b/src/views/HistoricalAlerts/AlertActions/index.tsx @@ -0,0 +1,56 @@ +import { useCallback } from 'react'; +import { generatePath } from 'react-router-dom'; +import { CopyLineIcon } from '@ifrc-go/icons'; +import { Button } from '@ifrc-go/ui'; +import { useTranslation } from '@ifrc-go/ui/hooks'; + +import Link from '#components/Link'; +import { AlertInformationsQuery } from '#generated/types/graphql'; +import useAlert from '#hooks/useAlert'; +import routes from '#routes'; + +import i18n from './i18n.json'; +import styles from './styles.module.css'; + +type AlertType = NonNullable['alerts']>['items']>[number]; + +export interface Props { + data: AlertType; +} +function AlertActions(props: Props) { + const { data } = props; + const strings = useTranslation(i18n); + const alert = useAlert(); + + const url = generatePath( + routes.alertDetails.absolutePath, + { alertId: data.id }, + ); + + const handleClick = useCallback(() => { + navigator.clipboard.writeText(`${window.location.origin}${url}`); + alert.show('Link copied to clipboard'); + }, [url, alert]); + + return ( +
+ + {strings.alertTableViewDetailsTitle} + + +
+ ); +} + +export default AlertActions; diff --git a/src/views/HistoricalAlerts/AlertActions/styles.module.css b/src/views/HistoricalAlerts/AlertActions/styles.module.css new file mode 100644 index 00000000..39705f67 --- /dev/null +++ b/src/views/HistoricalAlerts/AlertActions/styles.module.css @@ -0,0 +1,6 @@ +.alert-actions{ + display: flex; + gap: var(--go-ui-spacing-sm); +} + + diff --git a/src/views/HistoricalAlerts/i18n.json b/src/views/HistoricalAlerts/i18n.json new file mode 100644 index 00000000..03739d60 --- /dev/null +++ b/src/views/HistoricalAlerts/i18n.json @@ -0,0 +1,35 @@ +{ + "namespace": "historicalAlerts", + "strings": { + "allOngoingAlertTitle":"Past 3 Months Alerts ({numAppeals}) ", + "historicalAlertTableEventTitle":"Event" , + "historicalAlertTableCategoryTitle":"Event Categories", + "historicalAlertTableRegionTitle":"Region", + "historicalAlertTableCountryTitle":"Country", + "historicalAlertTableActionsTitle":"Actions", + "historicalAlertTableAdminsTitle":"Admin1s", + "historicalAlertTableSentLabel":"Sent", + "tableViewAllSources": "View All Sources", + "historicalAlertTitle": "IFRC Alert Hub - Historical Alerts", + "historicalAlert": "Historical Alerts", + "filterCountriesPlaceholder": "All Countries", + "filterAdmin1Placeholder": "All Admin1", + "filterUrgencyPlaceholder": "All Urgency Types", + "filterSeverityPlaceholder": "All Severity Types", + "filterCertaintyPlaceholder": "All Certainty Types", + "filterCountriesLabel": "Country", + "filterAdmin1Label": "Admin1", + "filterUrgencyLabel": "Urgency Level", + "filterSeverityLabel": "Severity Level", + "filterCertaintyLabel": "Certainty Level", + "filterRegionsLabel": "Regions", + "filterRegionsPlaceholder": "All Regions", + "filterCategoriesLabel": "Event Categories", + "filterCategoriesPlaceholder": "All Event Categories", + "filterStartDateFrom":"Start date from", + "filterStartDateTo":"Start date To", + "filterApply": "Apply", + "filterClear": "Clear", + "historicalAlertDescription": "IFRC Alert Hub provides global emergency alerts, empowering communities to protect lives and livelihoods. Easily access and filter past alerts from the latest months to stay informed." + } +} diff --git a/src/views/HistoricalAlerts/index.tsx b/src/views/HistoricalAlerts/index.tsx new file mode 100644 index 00000000..3c250d3a --- /dev/null +++ b/src/views/HistoricalAlerts/index.tsx @@ -0,0 +1,538 @@ +import { + ComponentType, + HTMLProps, + useCallback, + useMemo, + useState, +} from 'react'; +import { + gql, + useQuery, +} from '@apollo/client'; +import { ChevronRightLineIcon } from '@ifrc-go/icons'; +import { + Button, + Container, + DateInput, + DateOutput, + DateOutputProps, + MultiSelectInput, + Pager, + SelectInput, + Table, +} from '@ifrc-go/ui'; +import { SortContext } from '@ifrc-go/ui/contexts'; +import { useTranslation } from '@ifrc-go/ui/hooks'; +import { + createElementColumn, + createListDisplayColumn, + createStringColumn, + resolveToString, +} from '@ifrc-go/ui/utils'; +import { + doesObjectHaveNoData, + isDefined, + isNotDefined, +} from '@togglecorp/fujs'; + +import Link from '#components/Link'; +import Page from '#components/Page'; +import { + AlertEnumsAndAllCountryListQuery, + AlertEnumsAndAllCountryListQueryVariables, + AlertEnumsQuery, + AlertFilter, + FilteredAdminListQuery, + FilteredAdminListQueryVariables, + HistoricalAlertInformationsQuery, + HistoricalAlertInformationsQueryVariables, + OffsetPaginationInput, +} from '#generated/types/graphql'; +import useFilterState from '#hooks/useFilterState'; +import { DATE_FORMAT } from '#utils/constants'; +import { + stringIdSelector, + stringNameSelector, +} from '#utils/selectors'; +import AlertFilters from '#views/Home/AlertFilters'; + +import AlertActions, { type Props as AlertActionsProps } from './AlertActions'; + +import i18n from './i18n.json'; +import styles from './styles.module.css'; + +const HISTORICAL_ALERT_INFORMATIONS = gql` + query HistoricalAlertInformations( + $pagination: OffsetPaginationInput, + $filters: AlertFilter, + ) { + public { + id + historicalAlerts( + pagination: $pagination, + filters: $filters, + ) { + limit + offset + count + items { + id + country { + id + name + region { + id + name + } + } + admin1s { + id + name + } + sent + info { + id + event + alertId + categoryDisplay + } + } + } + } + } +`; + +const ALERT_ENUMS_AND_ALL_COUNTRY = gql` +query AlertEnumsAndAllCountryList { + enums { + AlertInfoCertainty { + key + label + } + AlertInfoUrgency { + key + label + } + AlertInfoSeverity { + key + label + } + AlertInfoCategory { + key + label + } + } + public { + id + allCountries { + name + id + } + } +} +`; + +const ADMIN_LIST = gql` +query FilteredAdminList($filters:Admin1Filter, $pagination: OffsetPaginationInput) { + public { + id + admin1s(filters: $filters, pagination: $pagination) { + items { + id + name + countryId + alertCount + } + } + } +} +`; + +type AdminOption = NonNullable['admin1s']>['items']>[number]; + +type Urgency = NonNullable[number]; +type Severity = NonNullable[number]; +type Certainty = NonNullable[number]; +type Category = NonNullable[number]; + +type AlertType = NonNullable['historicalAlerts']>['items']>[number]; +type Admin1 = AlertType['admin1s'][number]; + +const adminKeySelector = (admin1: AdminOption) => admin1.id; +const urgencyKeySelector = (urgency: Urgency) => urgency.key; +const severityKeySelector = (severity: Severity) => severity.key; +const certaintyKeySelector = (certainty: Certainty) => certainty.key; +const labelSelector = (alert: AlertFilters) => alert.label; +const categoryKeySelector = (category: Category) => category.key; + +const alertKeySelector = (item: AlertType) => item.id; +const PAGE_SIZE = 20; + +type NewFilter = Omit & AlertFilter['infos'] & { + startDateAfter?: string; + startDateBefore?: string; +}; + +// eslint-disable-next-line import/prefer-default-export +export function Component() { + const strings = useTranslation(i18n); + + const [finalFilter, setFinalFilter] = useState(); + + const { + sortState, + limit, + page, + rawFilter, + setPage, + filter, + setFilterField, + filtered, + offset, + resetFilter, + } = useFilterState({ + pageSize: PAGE_SIZE, + filter: {}, + }); + + const variables = useMemo<{ + filters: AlertFilter | undefined, + pagination: OffsetPaginationInput, + }>(() => { + const sentFilter = finalFilter?.startDateBefore || finalFilter?.startDateAfter ? { + range: { + ...(finalFilter?.startDateBefore && { end: finalFilter.startDateBefore }), + ...(finalFilter?.startDateAfter && { start: finalFilter.startDateAfter }), + }, + } : undefined; + return { + pagination: { + offset, + limit, + }, + filters: finalFilter ? { + DISTINCT: true, + infos: { + urgency: finalFilter?.urgency, + severity: finalFilter?.severity, + certainty: finalFilter?.certainty, + category: finalFilter?.category, + }, + country: isDefined(finalFilter?.country?.pk) + ? { pk: finalFilter.country.pk } : undefined, + admin1: finalFilter?.admin1, + sent: sentFilter, + } : undefined, + }; + }, [ + limit, + offset, + finalFilter, + ]); + + const handleApplyFilters = useCallback(() => { + if (doesObjectHaveNoData(rawFilter)) { + setFinalFilter(undefined); + } else { + const updatedFilter = { + ...rawFilter, + sent: rawFilter.startDateBefore || rawFilter.startDateAfter ? { + range: { + ...(rawFilter.startDateBefore && { end: rawFilter.startDateBefore }), + ...(rawFilter.startDateAfter && { start: rawFilter.startDateAfter }), + }, + } : {}, + }; + setFinalFilter(updatedFilter); + } + setPage(1); + }, [rawFilter, setPage]); + + const handleResetFilters = useCallback(() => { + resetFilter(); + setFinalFilter(undefined); + }, [ + resetFilter, + ]); + + const { + loading: alertInfoLoading, + previousData, + data: alertInfosResponse = previousData, + error: alertInfoError, + } = useQuery( + HISTORICAL_ALERT_INFORMATIONS, + { + skip: isNotDefined(variables), + variables, + }, + ); + + const { + data: alertEnumsResponse, + } = useQuery( + ALERT_ENUMS_AND_ALL_COUNTRY, + ); + + const adminQueryVariables = useMemo( + () => { + if (isNotDefined(filter.country)) { + return { + filters: undefined, + // FIXME: Implement search select input + pagination: { + offset: 0, + limit: 500, + }, + }; + } + + return { + filters: { + country: { pk: filter.country.pk }, + }, + // FIXME: Implement search select input + pagination: { + offset: 0, + limit: 500, + }, + }; + }, + [filter.country], + ); + + const { + data: adminResponse, + } = useQuery( + ADMIN_LIST, + { variables: adminQueryVariables, skip: isNotDefined(filter.country) }, + ); + + const data = alertInfosResponse?.public.historicalAlerts; + + const columns = useMemo( + () => ([ + createStringColumn( + 'event', + strings.historicalAlertTableEventTitle, + (item) => item.info?.event, + { columnClassName: styles.event }, + ), + createStringColumn( + 'category', + strings.historicalAlertTableCategoryTitle, + (item) => item.info?.categoryDisplay, + { columnClassName: styles.category }, + ), + createStringColumn( + 'region', + strings.historicalAlertTableRegionTitle, + (item) => (item.country.region.name), + { columnClassName: styles.region }, + + ), + createStringColumn( + 'country', + strings.historicalAlertTableCountryTitle, + (item) => (item.country.name), + { columnClassName: styles.country }, + ), + createListDisplayColumn>( + 'admin1s', + strings.historicalAlertTableAdminsTitle, + (item) => ({ + list: item.admin1s, + keySelector: ({ id }) => id, + renderer: 'span' as unknown as ComponentType>, + rendererParams: ({ name }) => ({ children: name }), + }), + { columnClassName: styles.admins }, + ), + createElementColumn( + 'sent', + strings.historicalAlertTableSentLabel, + DateOutput, + (_, item) => ({ + value: item.sent, + format: DATE_FORMAT, + }), + { + sortable: true, + columnClassName: styles.sent, + }, + ), + createElementColumn( + 'actions', + strings.historicalAlertTableActionsTitle, + AlertActions, + (_, item) => ({ data: item }), + { + columnClassName: styles.actions, + cellRendererClassName: styles.actions, + }, + ), + ]), + [ + strings.historicalAlertTableEventTitle, + strings.historicalAlertTableCategoryTitle, + strings.historicalAlertTableRegionTitle, + strings.historicalAlertTableCountryTitle, + strings.historicalAlertTableAdminsTitle, + strings.historicalAlertTableSentLabel, + strings.historicalAlertTableActionsTitle, + ], + ); + const heading = resolveToString( + strings.allOngoingAlertTitle, + { numAppeals: data?.count ?? '--' }, + ); + + const handleCountryFilterChange = useCallback((countryId: string | undefined) => { + setFilterField(countryId ? { pk: countryId } : undefined, 'country'); + }, [setFilterField]); + + return ( + + + )} + > + {strings.tableViewAllSources} + + )} + overlayPending + pending={alertInfoLoading} + errored={isDefined(alertInfoError)} + errorMessage={alertInfoError?.message} + footerActions={isDefined(data) && ( + + )} + filters={( + <> + + + + + + + + +
+ + +
+ + )} + > + + + + + + ); +} + +Component.displayName = 'HistoricalAlerts'; diff --git a/src/views/HistoricalAlerts/styles.module.css b/src/views/HistoricalAlerts/styles.module.css new file mode 100644 index 00000000..d7b84fd0 --- /dev/null +++ b/src/views/HistoricalAlerts/styles.module.css @@ -0,0 +1,84 @@ +.historical-alerts { + .pager { + button { + flex-shrink: 0 !important; + border-radius: 2rem !important; + padding: 0 var(--go-ui-spacing-sm); + width: fit-content !important; + min-width: 2rem; + line-height: 1; + } + } + .alerts-table { + overflow: auto; + + .alert-info { + display: flex; + align-items: flex-end; + + .alert-icon { + font-size: var(--go-ui-font-size-md); + } + } + + .main-content { + flex-grow: 1; + overflow: auto; + } + + .event { + width: 0%; + min-width: 8rem; + } + + .category { + width: 0%; + min-width: 5rem; + } + + .region { + width: 0%; + min-width: 7rem; + } + + .country { + width: 0%; + min-width: 8rem; + } + + .admins { + min-width: 14rem; + } + + .sent { + width: 0; + min-width: 7rem; + } + + .actions { + width: 0; + min-width: 10rem; + color: var(--go-ui-color-text); + font-weight: var(--go-ui-font-weight-medium); + } + + .sources { + display: flex; + align-items: center; + text-decoration: none; + color: var(--go-ui-color-text); + font-weight: var(--go-ui-font-weight-medium); + } + + .sources:hover { + text-decoration: underline; + color: var(--go-ui-color-primary-red); + } + + .filter-button { + display: flex; + gap: var(--go-ui-spacing-md); + align-items: flex-end; + } + } +} \ No newline at end of file diff --git a/src/views/Home/AlertFilters/index.tsx b/src/views/Home/AlertFilters/index.tsx index c4dc0b02..b8c270ee 100644 --- a/src/views/Home/AlertFilters/index.tsx +++ b/src/views/Home/AlertFilters/index.tsx @@ -55,65 +55,65 @@ const categoryLabelSelector = (category: Category) => category.label; const ALERT_ENUMS = gql` query AlertEnums { enums { - AlertInfoCertainty { - key - label - } - AlertInfoUrgency { - label - key - } - AlertInfoSeverity { - key - label - } - AlertInfoCategory { - key - label - } + AlertInfoCertainty { + key + label + } + AlertInfoUrgency { + label + key + } + AlertInfoSeverity { + key + label + } + AlertInfoCategory { + key + label + } } }`; const ADMIN_LIST = gql` query FilteredAdminList($filters:Admin1Filter, $pagination: OffsetPaginationInput) { public { - id - admin1s(filters: $filters, pagination: $pagination) { - items { - id - name - countryId - alertCount + id + admin1s(filters: $filters, pagination: $pagination) { + items { + id + name + countryId + alertCount + } } - } } - } +} `; const REGION_LIST = gql` query RegionList { public { id - regions { - items { - id - name - ifrcGoId + regions { + items { + id + name + ifrcGoId + } } - } } - } +} `; const ALL_COUNTRY_LIST = gql` query AllCountryList { - public { - id - allCountries { - name - id + public { + id + allCountries { + name + id + } } - } } `; @@ -131,16 +131,12 @@ function AlertFilters(props: Props) { selectedUrgencyTypes, selectedCertaintyTypes, activeRegionId, - // startDateFrom, - // startDateTo, setActiveCountryId, setActiveAdmin1Id, setSelectedSeverityTypes, setSelectedUrgencyTypes, setSelectedCertaintyTypes, setActiveRegionId, - // setStartDateFrom, - // setStartDateTo, selectedCategoryTypes, setSelectedCategoryTypes, } = useContext(AlertDataContext); @@ -231,21 +227,6 @@ function AlertFilters(props: Props) { value={selectedCertaintyTypes} onChange={setSelectedCertaintyTypes} /> - {/* - Add these filter after adding Historical alerts - - - */} {variant === 'table' && ( ({ countryId, alertFilters: { - severity: alertFilters.severity, - certainty: alertFilters.certainty, - urgency: alertFilters.urgency, + DISTINCT: true, + infos: { + severity: alertFilters.infos?.severity, + certainty: alertFilters.infos?.certainty, + urgency: alertFilters.infos?.urgency, + }, sent: alertFilters.sent, }, }), diff --git a/src/views/Home/AlertsMap/Sidebar/CountryDetail/index.tsx b/src/views/Home/AlertsMap/Sidebar/CountryDetail/index.tsx index aac0fd6f..11b6e1f4 100644 --- a/src/views/Home/AlertsMap/Sidebar/CountryDetail/index.tsx +++ b/src/views/Home/AlertsMap/Sidebar/CountryDetail/index.tsx @@ -36,23 +36,23 @@ import styles from './styles.module.css'; const COUNTRY_DETAIL = gql` query CountryDetail($countryId: ID!) { - public { - id - country(pk: $countryId) { - id - bbox - name - iso3 - ifrcGoId - alertCount - admin1s { + public { id - countryId - filteredAlertCount - name - } + country(pk: $countryId) { + id + bbox + name + iso3 + ifrcGoId + alertCount + admin1s { + id + countryId + filteredAlertCount + name + } + } } - } } `; diff --git a/src/views/Home/AlertsMap/i18n.json b/src/views/Home/AlertsMap/i18n.json index 64c9deea..dcca6882 100644 --- a/src/views/Home/AlertsMap/i18n.json +++ b/src/views/Home/AlertsMap/i18n.json @@ -6,6 +6,7 @@ "ongoingAlertCountries": "Ongoing Alert Countries", "backToAlertsLabel": "Back to Alerts", "alertViewDetails": "View Details", - "alertInfo": "The IFRC AlertHub shows current warnings from official alerting agencies. These warnings have a start time (when the event might happen) and an end time (when it's expected to be over). The IFRC Alert Hub shows warnings that are happening right now (their start time has already passed) but aren't finished yet (their end time hasn't come yet)." + "alertInfo": "The IFRC AlertHub shows current warnings from official alerting agencies. These warnings have a start time (when the event might happen) and an end time (when it's expected to be over). The IFRC Alert Hub shows warnings that are happening right now (their start time has already passed) but aren't finished yet (their end time hasn't come yet).", + "alertNewSubscription": "New Subscription" } } diff --git a/src/views/Home/AlertsMap/index.tsx b/src/views/Home/AlertsMap/index.tsx index 0154322c..cf88bdce 100644 --- a/src/views/Home/AlertsMap/index.tsx +++ b/src/views/Home/AlertsMap/index.tsx @@ -77,6 +77,7 @@ type AlertPointProperties = { export function Component() { const strings = useTranslation(i18n); const alertFilters = useAlertFilters(); + const { activeAdmin1Id, activeCountryId, @@ -185,15 +186,17 @@ export function Component() { withHeaderBorder childrenContainerClassName={styles.mainContent} actions={( - - )} - > - {strings.mapViewAllSources} - +
+ + )} + > + {strings.mapViewAllSources} + +
)} overlayPending pending={countryListLoading} @@ -202,7 +205,6 @@ export function Component() { contentViewType="grid" numPreferredGridContentColumns={3} filters={} - withGridViewInFilter > { setFilter({ ...alertFilters, + DISTINCT: true, country: isDefined(activeCountryId) ? { pk: activeCountryId } : undefined, admin1: activeAdmin1Id, region: activeRegionId, - category: selectedCategoryTypes, + infos: { + category: selectedCategoryTypes, + severity: selectedSeverityTypes, + certainty: selectedCertaintyTypes, + urgency: selectedUrgencyTypes, + }, sent: isDefined(startDateFrom) && isDefined(startDateTo) ? { range: { end: startDateTo, @@ -151,6 +160,9 @@ export function Component() { activeAdmin1Id, activeRegionId, selectedCategoryTypes, + selectedSeverityTypes, + selectedCertaintyTypes, + selectedUrgencyTypes, startDateFrom, startDateTo, ], @@ -284,17 +296,18 @@ export function Component() { )} withHeaderBorder - withGridViewInFilter actions={( - - )} - > - {strings.tableViewAllSources} - +
+ + )} + > + {strings.tableViewAllSources} + +
)} overlayPending pending={alertInfoLoading} diff --git a/src/views/Home/AlertsTable/styles.module.css b/src/views/Home/AlertsTable/styles.module.css index 823d1d34..7662920f 100644 --- a/src/views/Home/AlertsTable/styles.module.css +++ b/src/views/Home/AlertsTable/styles.module.css @@ -51,16 +51,21 @@ font-weight: var(--go-ui-font-weight-medium); } - .sources { + .links { display: flex; - align-items: center; - text-decoration: none; - color: var(--go-ui-color-text); - font-weight: var(--go-ui-font-weight-medium); - } + gap: var(--go-ui-spacing-lg); - .sources:hover { - text-decoration: underline; - color: var(--go-ui-color-primary-red); + .sources { + display: flex; + align-items: center; + text-decoration: none; + color: var(--go-ui-color-text); + font-weight: var(--go-ui-font-weight-medium); + } + + .sources:hover { + text-decoration: underline; + color: var(--go-ui-color-primary-red); + } } } \ No newline at end of file diff --git a/src/views/Home/i18n.json b/src/views/Home/i18n.json index c791da0c..8eea42b3 100644 --- a/src/views/Home/i18n.json +++ b/src/views/Home/i18n.json @@ -5,6 +5,12 @@ "homeHeading": "IFRC Alert Hub", "homeDescription": "IFRC Alert Hub provides global emergency alerts, empowering communities to protect lives and livelihoods.", "mapTabTitle": "Map", - "tableTabTitle": "Table" + "tableTabTitle": "Table", + "addSubscription": "Add Subscription to receive real-time alerts ", + "addSubscriptionDescription": "With real-time monitoring of potential risks and emergency events, receive timely and accurate alerts.", + "useApi": "Use API to rebroadcast CAP alerts", + "useApiDescription": "With simple yet powerful API endpoints, you can tailor the alerts to suit your users' needs.", + "alertNewSubscription": "Subscribe Alerts ", + "alertApiReference": "API Reference" } } diff --git a/src/views/Home/index.tsx b/src/views/Home/index.tsx index b47ce863..9ae2e002 100644 --- a/src/views/Home/index.tsx +++ b/src/views/Home/index.tsx @@ -4,13 +4,23 @@ import { useState, } from 'react'; import { Outlet } from 'react-router-dom'; -import { NavigationTabList } from '@ifrc-go/ui'; -import { useTranslation } from '@ifrc-go/ui/hooks'; +import { + Button, + Container, + NavigationTabList, +} from '@ifrc-go/ui'; +import { + useBooleanState, + useTranslation, +} from '@ifrc-go/ui/hooks'; import { isDefined, isNotDefined, } from '@togglecorp/fujs'; +import alerthubApi from '#assets/icons/alerthub_api.svg'; +import alerthubLogo from '#assets/icons/alerthub_Logo.png'; +import Link from '#components/Link'; import NavigationTab from '#components/NavigationTab'; import Page from '#components/Page'; import { @@ -18,6 +28,7 @@ import { CountryDetailQuery, } from '#generated/types/graphql'; import useUrlSearchState from '#hooks/useUrlSearchState'; +import NewSubscriptionModal from '#views/NewSubscriptionModal'; import AlertDataContext, { AlertDataContextProps } from './AlertDataContext'; @@ -55,16 +66,22 @@ function convertIdToUrlQuery(urlQuery: string | undefined | null) { } type SafeExtract = Extract; -type ApplicableAlertFilterKey = SafeExtract; -type ApplicableAlertFilter = Pick & { - alert: string | undefined; - startDateFrom: string | undefined; - startDateTo: string | undefined; -}; +type DirectAlertFilterKeys = SafeExtract; +type InfosAlertFilters = NonNullable; +type InfosAlertFilterKeys = SafeExtract; +type ApplicableAlertFilterKey = DirectAlertFilterKeys | InfosAlertFilterKeys; + +type ApplicableAlertFilter = Pick + & Pick + & { + alert: string | undefined; + startDateFrom: string | undefined; + startDateTo: string | undefined; + }; -type CompbinedAlertFilterKey = ApplicableAlertFilterKey | 'alert' | 'startDateFrom' | 'startDateTo'; -const filterKeys: CompbinedAlertFilterKey[] = ['country', 'admin1', 'urgency', 'region', 'severity', 'category', 'certainty', 'alert', 'startDateTo', 'startDateFrom']; +type CombinedAlertFilterKey = ApplicableAlertFilterKey | 'alert' | 'startDateFrom' | 'startDateTo'; +const filterKeys: CombinedAlertFilterKey[] = ['country', 'admin1', 'region', 'urgency', 'severity', 'category', 'certainty', 'alert', 'startDateTo', 'startDateFrom']; // eslint-disable-next-line import/prefer-default-export export function Component() { @@ -73,28 +90,40 @@ export function Component() { const [ filters, setFilters, - ] = useUrlSearchState( + ] = useUrlSearchState( filterKeys, - (urlValues) => ({ - country: isDefined(urlValues.country) ? { pk: urlValues.country } : undefined, - admin1: convertUrlQueryToId(urlValues.admin1), - region: convertUrlQueryToId(urlValues.region), - category: convertUrlQueryToEnumList(urlValues.category), - urgency: convertUrlQueryToEnumList(urlValues.urgency), - severity: convertUrlQueryToEnumList(urlValues.severity), - certainty: convertUrlQueryToEnumList(urlValues.certainty), - alert: convertUrlQueryToId(urlValues.alert), - startDateTo: convertUrlQueryToId(urlValues.startDateTo), - startDateFrom: convertUrlQueryToId(urlValues.startDateFrom), - }), + (urlValues) => { + const category: NonNullable['category'] = convertUrlQueryToEnumList(urlValues.category); + const urgency: NonNullable['urgency'] = convertUrlQueryToEnumList(urlValues.urgency); + const severity: NonNullable['severity'] = convertUrlQueryToEnumList(urlValues.severity); + const certainty: NonNullable['certainty'] = convertUrlQueryToEnumList(urlValues.certainty); + + return { + country: isDefined(urlValues.country) ? { pk: urlValues.country } : undefined, + admin1: convertUrlQueryToId(urlValues.admin1), + region: convertUrlQueryToId(urlValues.region), + infos: (isDefined(category) + || isDefined(urgency) + || isDefined(severity) || isDefined(certainty)) + ? ({ + category, + urgency, + severity, + certainty, + }) : undefined, + alert: convertUrlQueryToId(urlValues.alert), + startDateTo: convertUrlQueryToId(urlValues.startDateTo), + startDateFrom: convertUrlQueryToId(urlValues.startDateFrom), + }; + }, (filterValues) => ({ country: convertIdToUrlQuery(filterValues.country?.pk), admin1: convertIdToUrlQuery(filterValues.admin1), region: convertIdToUrlQuery(filterValues.region), - category: convertEnumListToUrlQuery(filterValues.category), - urgency: convertEnumListToUrlQuery(filterValues.urgency), - severity: convertEnumListToUrlQuery(filterValues.severity), - certainty: convertEnumListToUrlQuery(filterValues.certainty), + category: convertEnumListToUrlQuery(filterValues.infos?.category), + urgency: convertEnumListToUrlQuery(filterValues.infos?.urgency), + severity: convertEnumListToUrlQuery(filterValues.infos?.severity), + certainty: convertEnumListToUrlQuery(filterValues.infos?.certainty), alert: convertUrlQueryToId(filterValues.alert), startDateTo: convertUrlQueryToId(filterValues.startDateTo), startDateFrom: convertUrlQueryToId(filterValues.startDateFrom), @@ -122,13 +151,28 @@ export function Component() { ); const getFilterFieldSetterFn = useCallback( - (fieldKey: CompbinedAlertFilterKey) => ( + (fieldKey: CombinedAlertFilterKey) => ( (newValue: ApplicableAlertFilter[ApplicableAlertFilterKey]) => { setFilters( - (prevFilter) => ({ - ...prevFilter, - [fieldKey]: newValue, - }), + (prevFilter) => { + if (fieldKey === 'category' + || fieldKey === 'urgency' + || fieldKey === 'severity' + || fieldKey === 'certainty' + ) { + return ({ + ...prevFilter, + infos: { + ...prevFilter.infos, + [fieldKey]: newValue, + }, + }); + } + return ({ + ...prevFilter, + [fieldKey]: newValue, + }); + }, ); } ), @@ -155,16 +199,16 @@ export function Component() { activeAdmin1Details: isDefined(filters.admin1) ? activeAdmin1Details : undefined, setActiveAdmin1Details, - selectedUrgencyTypes: filters.urgency ?? undefined, + selectedUrgencyTypes: filters.infos?.urgency ?? undefined, setSelectedUrgencyTypes: getFilterFieldSetterFn('urgency'), - selectedSeverityTypes: filters.severity ?? undefined, + selectedSeverityTypes: filters.infos?.severity ?? undefined, setSelectedSeverityTypes: getFilterFieldSetterFn('severity'), - selectedCertaintyTypes: filters.certainty ?? undefined, + selectedCertaintyTypes: filters.infos?.certainty ?? undefined, setSelectedCertaintyTypes: getFilterFieldSetterFn('certainty'), - selectedCategoryTypes: filters.category ?? undefined, + selectedCategoryTypes: filters.infos?.category ?? undefined, setSelectedCategoryTypes: getFilterFieldSetterFn('category'), startDateFrom: filters.startDateFrom, @@ -181,6 +225,20 @@ export function Component() { getFilterFieldSetterFn, ], ); + const defaultSubscription = useMemo(() => ({ + filterAlertUrgencies: alertContextValue.selectedUrgencyTypes, + filterAlertCertainties: alertContextValue.selectedCertaintyTypes, + filterAlertSeverities: alertContextValue.selectedSeverityTypes, + filterAlertCategories: alertContextValue.selectedCategoryTypes, + filterAlertCountry: alertContextValue.activeCountryId, + filterAlertAdmin1s: alertContextValue.activeAdmin1Id + ? [alertContextValue.activeAdmin1Id] : [], + }), [alertContextValue]); + + const [showSubscriptionModal, { + setTrue: setShowSubscriptionModalTrue, + setFalse: setShowSubscriptionModalFalse, + }] = useBooleanState(false); return ( @@ -192,15 +250,93 @@ export function Component() { infoContainerClassName={styles.tabSection} mainSectionClassName={styles.content} info={( - - - {strings.mapTabTitle} - - - {strings.tableTabTitle} - - + <> + + + + {strings.alertNewSubscription} + + )} + /> + + + + + {showSubscriptionModal && ( + + )} + + + {strings.alertApiReference} + + )} + /> + + + + + +
+ + + {strings.mapTabTitle} + + + {strings.tableTabTitle} + + +
+ )} + > diff --git a/src/views/Home/styles.module.css b/src/views/Home/styles.module.css index f21f003f..1db31cba 100644 --- a/src/views/Home/styles.module.css +++ b/src/views/Home/styles.module.css @@ -7,7 +7,33 @@ .tab-section { display: flex; + flex-direction: column; + gap: var(--go-ui-spacing-xl); justify-content: center; + + .cards { + gap: var(--go-ui-spacing-xl); + + .card { + gap: var(--go-ui-spacing-lg); + border-radius: var(--go-ui-border-radius-lg); + box-shadow: var(--go-ui-box-shadow-md); + background-color: var(--go-ui-color-foreground); + overflow: auto; + + .cards-content { + display: flex; + align-items: center; + justify-content: space-between; + + .alert-image { + margin: 0 auto; + object-fit: contain; + height: 5rem; + } + } + } + } } .map-filter, diff --git a/src/views/Home/useAlertFilters.ts b/src/views/Home/useAlertFilters.ts index 73aabb2d..f46ea4cb 100644 --- a/src/views/Home/useAlertFilters.ts +++ b/src/views/Home/useAlertFilters.ts @@ -15,23 +15,39 @@ function useAlertFilters() { selectedUrgencyTypes, activeCountryId, activeAdmin1Id, + selectedCategoryTypes, } = useContext(AlertDataContext); + const certaintyDefined = isDefined(selectedCertaintyTypes) && selectedCertaintyTypes.length > 0; + const severityDefined = isDefined(selectedSeverityTypes) && selectedSeverityTypes.length > 0; + const urgencyDefined = isDefined(selectedUrgencyTypes) && selectedUrgencyTypes.length > 0; + const categoryDefined = isDefined(selectedCategoryTypes) && selectedCategoryTypes.length > 0; + const alertFilters = useMemo( () => ({ - certainty: isDefined(selectedCertaintyTypes) && selectedCertaintyTypes.length > 0 - ? selectedCertaintyTypes - : undefined, - severity: isDefined(selectedSeverityTypes) && selectedSeverityTypes.length > 0 - ? selectedSeverityTypes - : undefined, - urgency: isDefined(selectedUrgencyTypes) && selectedUrgencyTypes.length > 0 - ? selectedUrgencyTypes - : undefined, + infos: (certaintyDefined || severityDefined || urgencyDefined || categoryDefined) ? ({ + certainty: certaintyDefined + ? selectedCertaintyTypes + : undefined, + severity: severityDefined + ? selectedSeverityTypes + : undefined, + urgency: urgencyDefined + ? selectedUrgencyTypes + : undefined, + category: categoryDefined + ? selectedCategoryTypes + : undefined, + }) : undefined, country: isDefined(activeCountryId) ? { pk: activeCountryId } : undefined, admin1: isDefined(activeAdmin1Id) ? activeAdmin1Id : undefined, }), [ + certaintyDefined, + severityDefined, + urgencyDefined, + categoryDefined, + selectedCategoryTypes, activeAdmin1Id, activeCountryId, selectedUrgencyTypes, diff --git a/src/views/Login/i18n.json b/src/views/Login/i18n.json new file mode 100644 index 00000000..8fe03e0f --- /dev/null +++ b/src/views/Login/i18n.json @@ -0,0 +1,23 @@ +{ + "namespace": "login", + "strings": { + "loginTitle":"IFRC Alert-Hub - Login", + "loginHeader":"Login", + "loginSubHeader":"If you are staff, member or volunteer of the Red Cross Red Crescent Movement (National Societies, the IFRC and the ICRC) login with your IFRC AlertHub account email and password.", + "loginEmailUsername":"Email", + "loginPassword":"Password", + "loginRecoverTitle":"Recover password", + "loginShowUsernameTitle":"Show me my username", + "loginResendValidation":"Re-send validation email", + "loginResendValidationTitle":"I didn't get my validation email", + "loginForgotUserPass":"Forgot your password?", + "loginInvalid":"Invalid username or password", + "loginErrorMessage":"Error: {message}", + "loginButton":"Login", + "loginDontHaveAccount":"Don’t have an account? {registerLink}", + "loginCreateAccountTitle":"Create new account", + "loginRegister":"Register", + "loginFailureMessage": "Failed to login!", + "loginSuccessfully": "Logged in successfully!" + } +} diff --git a/src/views/Login/index.tsx b/src/views/Login/index.tsx new file mode 100644 index 00000000..7a857eb2 --- /dev/null +++ b/src/views/Login/index.tsx @@ -0,0 +1,243 @@ +import { + useCallback, + useContext, +} from 'react'; +import { useNavigate } from 'react-router-dom'; +import { + gql, + useMutation, +} from '@apollo/client'; +import { + Button, + PasswordInput, + TextInput, +} from '@ifrc-go/ui'; +import { useTranslation } from '@ifrc-go/ui/hooks'; +import { resolveToComponent } from '@ifrc-go/ui/utils'; +import { + createSubmitHandler, + emailCondition, + getErrorObject, + lengthGreaterThanCondition, + lengthSmallerThanCondition, + type ObjectSchema, + PartialForm, + requiredStringCondition, + useForm, +} from '@togglecorp/toggle-form'; + +import Link from '#components/Link'; +import Page from '#components/Page'; +import UserContext from '#contexts/user'; +import { + LoginMutation, + LoginMutationVariables, + UserLoginInput, +} from '#generated/types/graphql'; +import useAlert from '#hooks/useAlert'; + +import i18n from './i18n.json'; +import styles from './styles.module.css'; + +const LOGIN = gql` + mutation Login($data: UserLoginInput!) { + public { + login(data: $data) { + ok + errors + result { + id + displayName + firstName + email + lastName + } + } + } + } +`; + +type FormType = PartialForm; +type FormSchema = ObjectSchema; +type FormSchemaFields = ReturnType; + +const formSchema: FormSchema = { + fields: (): FormSchemaFields => ({ + email: { + required: true, + validations: [ + emailCondition, + ], + requiredValidation: requiredStringCondition, + }, + password: { + required: true, + validations: [ + lengthGreaterThanCondition(4), + lengthSmallerThanCondition(129), + ], + requiredValidation: requiredStringCondition, + }, + }), +}; + +const defaultFormValue: FormType = {}; + +// eslint-disable-next-line import/prefer-default-export +export function Component() { + const strings = useTranslation(i18n); + + const alert = useAlert(); + const navigate = useNavigate(); + const { setUserAuth: setUser } = useContext(UserContext); + + const { + pristine, + value: formValue, + setFieldValue, + error, + setError, + validate, + } = useForm(formSchema, { value: defaultFormValue }); + + const fieldError = getErrorObject(error); + + const [ + triggerLogin, + { loading: loginPending }, + ] = useMutation( + LOGIN, + { + onCompleted: (loginResponse) => { + const response = loginResponse?.public?.login; + if (!response) { + return; + } + + if (response.ok) { + setUser({ + firstName: response.result?.firstName, + lastName: response.result?.lastName, + displayName: response.result?.displayName, + email: response.result?.email, + }); + alert.show( + strings.loginSuccessfully, + { variant: 'success' }, + ); + navigate('/'); + } else { + alert.show( + strings.loginFailureMessage, + { variant: 'danger' }, + ); + } + }, + onError: () => { + alert.show( + strings.loginFailureMessage, + { variant: 'danger' }, + ); + }, + }, + ); + + const handleFormSubmit = useCallback(() => { + const handler = createSubmitHandler( + validate, + setError, + (val) => { + triggerLogin({ + variables: { + data: val as UserLoginInput, + }, + }); + }, + ); + handler(); + }, [ + setError, + triggerLogin, + validate, + ]); + + const registerInfo = resolveToComponent( + strings.loginDontHaveAccount, + { + registerLink: ( + + {strings.loginRegister} + + ), + }, + ); + + return ( + +
+
+ + +
+
+ + {strings.loginForgotUserPass} + + {/* + {strings.loginResendValidation} + */} +
+
+ +
+ {registerInfo} +
+
+ +
+ ); +} + +Component.displayName = 'Login'; diff --git a/src/views/Login/styles.module.css b/src/views/Login/styles.module.css new file mode 100644 index 00000000..823b2c65 --- /dev/null +++ b/src/views/Login/styles.module.css @@ -0,0 +1,39 @@ +.login { + .main-section { + display: flex; + justify-content: center; + + .form { + display: flex; + flex-direction: column; + flex-grow: 1; + gap: var(--go-ui-spacing-xl); + max-width: var(--go-ui-width-content-max); + + .fields { + display: flex; + flex-direction: column; + gap: var(--go-ui-spacing-xl); + } + + .utility-links { + display: flex; + flex-direction: column; + gap: var(--go-ui-spacing-sm); + align-items: flex-end; + } + + .actions { + display: flex; + flex-direction: column; + gap: var(--go-ui-spacing-lg); + align-items: center; + + .register { + display: flex; + gap: var(--go-ui-spacing-sm); + } + } + } + } +} diff --git a/src/views/MySubscriptions/ActiveTableActions/i18n.json b/src/views/MySubscriptions/ActiveTableActions/i18n.json new file mode 100644 index 00000000..3d6053d0 --- /dev/null +++ b/src/views/MySubscriptions/ActiveTableActions/i18n.json @@ -0,0 +1,9 @@ +{ + "namespace": "SubscriptionActions", + "strings": { + "deleteSubscriptionActions": "Delete", + "archiveSubscriptionActions": "Archive", + "editSubscriptionActions": "Edit", + "confirmationMessage": "Are you sure want to delete the subscription?" + } +} diff --git a/src/views/MySubscriptions/ActiveTableActions/index.tsx b/src/views/MySubscriptions/ActiveTableActions/index.tsx new file mode 100644 index 00000000..a573690a --- /dev/null +++ b/src/views/MySubscriptions/ActiveTableActions/index.tsx @@ -0,0 +1,71 @@ +import { useCallback } from 'react'; +import { + DeleteBinSixLineIcon, + EditTwoLineIcon, + LayoutBottomLineIcon, + MoreOptionsIcon, +} from '@ifrc-go/icons'; +import { DropdownMenu } from '@ifrc-go/ui'; +import { useTranslation } from '@ifrc-go/ui/hooks'; + +import DropdownMenuItem from '#components/DropdownMenuItem'; + +import i18n from './i18n.json'; + +interface Props { + onSubscriptionRemove: () => void; + onArchiveClick?: () => void; + onEditClick: () => void; +} + +function ActiveTableActions(props: Props) { + const { + onSubscriptionRemove, + onArchiveClick, + onEditClick, + } = props; + + const strings = useTranslation(i18n); + + const handleDelete = useCallback(() => { + onSubscriptionRemove(); + }, [onSubscriptionRemove]); + + return ( + } + variant="tertiary" + withoutDropdownIcon + persistent + > + } + > + {strings.archiveSubscriptionActions} + + } + > + {strings.editSubscriptionActions} + + } + persist + > + {strings.deleteSubscriptionActions} + + + ); +} + +export default ActiveTableActions; diff --git a/src/views/MySubscriptions/ArchiveTableActions/i18n.json b/src/views/MySubscriptions/ArchiveTableActions/i18n.json new file mode 100644 index 00000000..b2a3c552 --- /dev/null +++ b/src/views/MySubscriptions/ArchiveTableActions/i18n.json @@ -0,0 +1,8 @@ +{ + "namespace": "SubscriptionActions", + "strings": { + "unarchiveSubscriptionActions": "Unarchive", + "deleteSubscriptionActions": "Delete", + "confirmationMessage": "Are you sure want to delete the subscription?" + } +} \ No newline at end of file diff --git a/src/views/MySubscriptions/ArchiveTableActions/index.tsx b/src/views/MySubscriptions/ArchiveTableActions/index.tsx new file mode 100644 index 00000000..1737c520 --- /dev/null +++ b/src/views/MySubscriptions/ArchiveTableActions/index.tsx @@ -0,0 +1,60 @@ +import { useCallback } from 'react'; +import { + DeleteBinSixLineIcon, + LayoutBottomLineIcon, + MoreOptionsIcon, +} from '@ifrc-go/icons'; +import { DropdownMenu } from '@ifrc-go/ui'; +import { useTranslation } from '@ifrc-go/ui/hooks'; + +import DropdownMenuItem from '#components/DropdownMenuItem'; + +import i18n from './i18n.json'; + +interface Props { + onSubscriptionRemove: () => void; + onUnArchive?: () => void; +} + +function ArchiveTableActions(props: Props) { + const { + onSubscriptionRemove, + onUnArchive, + } = props; + + const strings = useTranslation(i18n); + + const handleDelete = useCallback(() => { + onSubscriptionRemove(); + }, [onSubscriptionRemove]); + + return ( + } + variant="tertiary" + withoutDropdownIcon + persistent + > + } + > + {strings.unarchiveSubscriptionActions} + + } + persist + > + {strings.deleteSubscriptionActions} + + + ); +} + +export default ArchiveTableActions; diff --git a/src/views/MySubscriptions/SubscriptionDetail/AlertInfoItem/i18n.json b/src/views/MySubscriptions/SubscriptionDetail/AlertInfoItem/i18n.json new file mode 100644 index 00000000..cb92f611 --- /dev/null +++ b/src/views/MySubscriptions/SubscriptionDetail/AlertInfoItem/i18n.json @@ -0,0 +1,6 @@ +{ + "namespace": "alertInfo", + "strings": { + "alertInfoView": "View" + } +} \ No newline at end of file diff --git a/src/views/MySubscriptions/SubscriptionDetail/AlertInfoItem/index.tsx b/src/views/MySubscriptions/SubscriptionDetail/AlertInfoItem/index.tsx new file mode 100644 index 00000000..6a191f68 --- /dev/null +++ b/src/views/MySubscriptions/SubscriptionDetail/AlertInfoItem/index.tsx @@ -0,0 +1,45 @@ +import { Container } from '@ifrc-go/ui'; +import { useTranslation } from '@ifrc-go/ui/hooks'; + +import Link from '#components/Link'; + +import i18n from './i18n.json'; +import styles from './styles.module.css'; + +interface Props { + alertId: string; + alertTitle: string | undefined; + alertDescription?: string; +} + +function AlertInfoItem(props: Props) { + const strings = useTranslation(i18n); + + const { + alertId, + alertTitle, + alertDescription, + } = props; + + return ( + + {strings.alertInfoView} + + )} + footerContent={alertDescription} + /> + ); +} + +export default AlertInfoItem; diff --git a/src/views/MySubscriptions/SubscriptionDetail/AlertInfoItem/styles.module.css b/src/views/MySubscriptions/SubscriptionDetail/AlertInfoItem/styles.module.css new file mode 100644 index 00000000..78103f83 --- /dev/null +++ b/src/views/MySubscriptions/SubscriptionDetail/AlertInfoItem/styles.module.css @@ -0,0 +1,4 @@ +.alert-detail { + border-radius: var(--go-ui-border-radius-lg); + background-color: var(--go-ui-color-background); +} diff --git a/src/views/MySubscriptions/SubscriptionDetail/i18n.json b/src/views/MySubscriptions/SubscriptionDetail/i18n.json new file mode 100644 index 00000000..86f475dc --- /dev/null +++ b/src/views/MySubscriptions/SubscriptionDetail/i18n.json @@ -0,0 +1,17 @@ +{ + "namespace": "subscriptionDetail", + "strings": { + "subscriptionDetailTitle": "Subscription Detail", + "subscriptionHeading": "Subscription", + "filterStartDateFrom": "Start date from", + "filterStartDateTo": "Start date To", + "filterEmptyMessage": "Alerts not available!", + "subscriptionCountry": "Country", + "subscriptionAdmin1": "Admin1", + "subscriptionUrgency": "Urgency", + "subscriptionCertainty": "Certainty", + "subscriptionSeverity": "Severity", + "subscriptionCategory": "Category", + "susbcriptionEmptyMessage": "No alerts to display!" + } +} \ No newline at end of file diff --git a/src/views/MySubscriptions/SubscriptionDetail/index.tsx b/src/views/MySubscriptions/SubscriptionDetail/index.tsx new file mode 100644 index 00000000..26df5315 --- /dev/null +++ b/src/views/MySubscriptions/SubscriptionDetail/index.tsx @@ -0,0 +1,293 @@ +import { + useCallback, + useMemo, +} from 'react'; +import { useParams } from 'react-router-dom'; +import { + gql, + useQuery, +} from '@apollo/client'; +import { + Chip, + Container, + DateInput, + Pager, + RawList, + TextOutput, +} from '@ifrc-go/ui'; +import { useTranslation } from '@ifrc-go/ui/hooks'; +import { + isDefined, + isNotDefined, +} from '@togglecorp/fujs'; + +import Page from '#components/Page'; +import { + AlertFilter, + DetailsOfSubsQuery, + DetailsOfSubsQueryVariables, + UserAlertSubscriptionType, +} from '#generated/types/graphql'; +import useFilterState from '#hooks/useFilterState'; +import { stringIdSelector } from '#utils/selectors'; + +import AlertInfoItem from './AlertInfoItem'; + +import i18n from './i18n.json'; +import styles from './styles.module.css'; + +const PAGE_SIZE = 20; + +const SUBSCRIPTION_ALERT_LIST = gql` + query DetailsOfSubs( + $pk: ID!, + $pagination: OffsetPaginationInput, + $filters: AlertFilter, + ) { + private { + userAlertSubscription(pk: $pk) { + alerts ( + pagination: $pagination, + filters: $filters, + ){ + items { + id + info { + event + id + description + } + } + count + } + filterAlertUrgenciesDisplay + id + name + filterAlertUrgencies + filterAlertSeveritiesDisplay + filterAlertSeverities + filterAlertCountry { + id + name + } + filterAlertCountryId + filterAlertCertaintiesDisplay + filterAlertCertainties + filterAlertCategoriesDisplay + filterAlertCategories + filterAlertAdmin1sDisplay { + id + name + } + filterAlertAdmin1s + } + } + } +`; + +type AlertInfo = NonNullable['alerts']>['items'][number]; + +// eslint-disable-next-line import/prefer-default-export +export function Component() { + const { subscriptionId } = useParams(); + + const strings = useTranslation(i18n); + + const { + limit, + page, + setPage, + setFilterField, + offset, + filter, + } = useFilterState<{ + startDateAfter?: string, + startDateBefore?: string, + }>({ + pageSize: PAGE_SIZE, + filter: {}, + }); + + const variables = useMemo(() => { + if (!subscriptionId) { + return {} as DetailsOfSubsQueryVariables; + } + + const filters: AlertFilter = { + sent: { + range: { + start: filter.startDateAfter, + end: filter.startDateBefore, + }, + }, + }; + + const finalFilters = (isDefined(filter.startDateAfter) + || isDefined(filter.startDateBefore) + ) ? filters : {}; + + return { + pk: subscriptionId, + pagination: { + offset, + limit, + }, + filters: finalFilters, + }; + }, [ + subscriptionId, + limit, + offset, + filter, + ]); + + const { + previousData, + loading: alertLoading, + error: alertError, + data: alertSubscription = previousData, + } = useQuery( + SUBSCRIPTION_ALERT_LIST, + { + skip: isNotDefined(variables), + variables, + }, + ); + + const alertsData = alertSubscription?.private?.userAlertSubscription; + + const rendererParams = useCallback((_: string, value: AlertInfo) => ({ + alertId: value.id, + alertTitle: value.info?.event, + alertDescription: value.info?.description ?? undefined, + }), []); + + return ( + + + + + + )} + footerActions={isDefined(alertsData) && ( + + )} + pending={alertLoading} + errored={isDefined(alertError)} + overlayPending + > +
+ + )} + /> + admin.name, + ).join(', ')} + strongLabel + /> + )} + /> + + )} + /> + + )} + /> + + )} + /> + + )} + /> +
+ + + +
+ ); +} + +Component.displayName = 'SubscriptionDetail'; diff --git a/src/views/MySubscriptions/SubscriptionDetail/styles.module.css b/src/views/MySubscriptions/SubscriptionDetail/styles.module.css new file mode 100644 index 00000000..3d9333d8 --- /dev/null +++ b/src/views/MySubscriptions/SubscriptionDetail/styles.module.css @@ -0,0 +1,11 @@ +.subscription-detail { + .filters { + display: flex; + flex-wrap: wrap; + gap: var(--go-ui-spacing-xs); + + .filter-item { + font-size: var(--go-ui-font-size-xs); + } + } +} \ No newline at end of file diff --git a/src/views/MySubscriptions/SubscriptionTableItem/i18n.json b/src/views/MySubscriptions/SubscriptionTableItem/i18n.json new file mode 100644 index 00000000..bd8f896c --- /dev/null +++ b/src/views/MySubscriptions/SubscriptionTableItem/i18n.json @@ -0,0 +1,12 @@ +{ + "namespace": "subscriptionItem", + "strings": { + "subscriptionItemView": "View", + "subscriptionCountry": "Country", + "subscriptionAdmin1": "Admin1", + "subscriptionUrgency": "Urgency", + "subscriptionCertainty": "Certainty", + "subscriptionSeverity": "Severity", + "subscriptionCategory": "Category" + } +} \ No newline at end of file diff --git a/src/views/MySubscriptions/SubscriptionTableItem/index.tsx b/src/views/MySubscriptions/SubscriptionTableItem/index.tsx new file mode 100644 index 00000000..0c8d7118 --- /dev/null +++ b/src/views/MySubscriptions/SubscriptionTableItem/index.tsx @@ -0,0 +1,148 @@ +import { + Chip, + Container, + NumberOutput, + TextOutput, +} from '@ifrc-go/ui'; +import { useTranslation } from '@ifrc-go/ui/hooks'; + +import Link from '#components/Link'; + +import i18n from './i18n.json'; +import styles from './styles.module.css'; + +interface Props { + id: string; + name: string; + alertCount: number; + filterAlertUrgencies: string[]; + filterAlertCertainties: string[]; + filterAlertSeverities: string[]; + filterAlertCategories: string[]; + filterAlertCountry: string; + filterAlertAdmin1s: string[]; + actions: React.ReactNode; +} + +function SubscriptionTableItem(props: Props) { + const { + id, + name, + alertCount, + filterAlertUrgencies, + filterAlertCategories, + filterAlertCertainties, + filterAlertSeverities, + filterAlertAdmin1s, + filterAlertCountry, + actions, + } = props; + + const strings = useTranslation(i18n); + + return ( + + )} + actions={( + <> + + {strings.subscriptionItemView} + + {actions} + + )} + childrenContainerClassName={styles.content} + > + + )} + /> + + )} + /> + + )} + /> + + )} + /> + + )} + /> + + )} + /> + + ); +} + +export default SubscriptionTableItem; diff --git a/src/views/MySubscriptions/SubscriptionTableItem/styles.module.css b/src/views/MySubscriptions/SubscriptionTableItem/styles.module.css new file mode 100644 index 00000000..88054597 --- /dev/null +++ b/src/views/MySubscriptions/SubscriptionTableItem/styles.module.css @@ -0,0 +1,14 @@ +.subscription-detail { + border-radius: var(--go-ui-border-radius-lg); + background-color: var(--go-ui-color-background); + + .content { + display: flex; + flex-wrap: wrap; + gap: var(--go-ui-spacing-xs); + + .filter-item { + font-size: var(--go-ui-font-size-xs); + } + } +} diff --git a/src/views/MySubscriptions/i18n.json b/src/views/MySubscriptions/i18n.json new file mode 100644 index 00000000..5ce22e1c --- /dev/null +++ b/src/views/MySubscriptions/i18n.json @@ -0,0 +1,18 @@ +{ + "namespace": "mySubscriptions", + "strings": { + "mySubscription": "My Subscriptions", + "myNewSubscription": "New Subscription", + "createNewSubscription": "Create", + "sendViaEmailLabel": "Send via email", + "activeSubscriptionsTab": "Active Subscriptions", + "archivedSubscriptionTab": "Archived Subscriptions", + "subscriptionUnarchived": "Subscription unarchived.", + "subscriptionArchived": "Subscription archived.", + "subscriptionFailedToUpdate": "Failed to update subscription.", + "subscriptionDescription": "Customize your alerts with tailored filters, manage your subscriptions, and stay informed with relevant alerts delivered directly to your inbox.", + "subscriptionDeleted": "Subscription deleted.", + "subscriptionFailedToDelete": "Failed to delete subscription", + "subscriptionEmptyMessage": "No subscription available!" + } +} diff --git a/src/views/MySubscriptions/index.tsx b/src/views/MySubscriptions/index.tsx new file mode 100644 index 00000000..9f0fcaa3 --- /dev/null +++ b/src/views/MySubscriptions/index.tsx @@ -0,0 +1,485 @@ +import { + useCallback, + useMemo, + useState, +} from 'react'; +import { + gql, + useMutation, + useQuery, +} from '@apollo/client'; +import { AddLineIcon } from '@ifrc-go/icons'; +import { + Button, + Container, + Pager, + RawList, + Tab, + TabList, + TabPanel, + Tabs, +} from '@ifrc-go/ui'; +import { + useBooleanState, + useTranslation, +} from '@ifrc-go/ui/hooks'; +import { isDefined } from '@togglecorp/fujs'; + +import Page from '#components/Page'; +import { + AlertFilter, + AlertSubscriptionsQuery, + AlertSubscriptionsQueryVariables, + ArchiveUnArchiveSubscriptionMutation, + ArchiveUnArchiveSubscriptionMutationVariables, + DeleteSubscriptionMutation, + DeleteSubscriptionMutationVariables, + OffsetPaginationInput, + UserAlertSubscriptionFilter, + UserAlertSubscriptionType, +} from '#generated/types/graphql'; +import useAlert from '#hooks/useAlert'; +import useFilterState from '#hooks/useFilterState'; + +import NewSubscriptionModal from '../NewSubscriptionModal'; +import ActiveTableActions from './ActiveTableActions'; +import ArchiveTableActions from './ArchiveTableActions'; +import SubscriptionTableItem from './SubscriptionTableItem'; + +import i18n from './i18n.json'; +import styles from './styles.module.css'; + +const ALERT_SUBSCRIPTIONS = gql` + query AlertSubscriptions( + $pagination: OffsetPaginationInput, + $filters: UserAlertSubscriptionFilter, + ) { + private { + id + userAlertSubscriptions(pagination: $pagination, filters: $filters) { + count + limit + offset + items { + id + name + isActive + notifyByEmail + emailFrequency + emailFrequencyDisplay + totalAlertsCount + filterAlertAdmin1s + filterAlertAdmin1sDisplay { + id + name + } + filterAlertCategoriesDisplay + filterAlertCategories + filterAlertCertaintiesDisplay + filterAlertCertainties + filterAlertCountryId + filterAlertCountry { + id + name + } + filterAlertSeveritiesDisplay + filterAlertUrgenciesDisplay + filterAlertUrgencies + filterAlertSeverities + } + } + } + } +`; + +const DELETE_SUBSCRIPTION = gql` + mutation DeleteSubscription( + $subscriptionId: ID!, + ) { + private { + deleteUserAlertSubscription(id: $subscriptionId) { + ok + errors + } + id + } + } +`; + +const UPDATE_SUBSCRIPTION = gql` + mutation ArchiveUnArchiveSubscription( + $subscriptionId: ID!, + $data: UserAlertSubscriptionInput!, + ) { + private { + updateUserAlertSubscription( + id: $subscriptionId, + data: $data, + ) { + errors + ok + result { + id + name + isActive + } + } + } + } +`; + +const PAGE_SIZE = 10; + +const subscriptionKeySelector = (subscription: UserAlertSubscriptionType) => subscription.id; + +// eslint-disable-next-line import/prefer-default-export +export function Component() { + const strings = useTranslation(i18n); + + const alert = useAlert(); + + type TabKey = 'active' | 'archive'; + const [activeTab, setActiveTab] = useState('active'); + + const [ + selectedSubscription, + setSelectedSubscription, + ] = useState(); + + const [showSubscriptionModal, { + setTrue: setShowSubscriptionModalTrue, + setFalse: setShowSubscriptionModalFalse, + }] = useBooleanState(false); + + const { + page, + setPage, + limit, + offset, + } = useFilterState({ + pageSize: PAGE_SIZE, + filter: {}, + }); + + const variables = useMemo<{ + pagination: OffsetPaginationInput, + filters: UserAlertSubscriptionFilter, + }>(() => ({ + pagination: { + offset, + limit, + }, + filters: { + isActive: { + exact: activeTab === 'active', + }, + }, + }), [ + activeTab, + limit, + offset, + ]); + + const { + previousData, + data: alertSubscriptions = previousData, + loading: alertSubscriptionLoading, + error: alertSubscriptionError, + refetch, + } = useQuery( + ALERT_SUBSCRIPTIONS, + { + variables, + }, + ); + + const data = alertSubscriptions?.private.userAlertSubscriptions; + + const [ + triggerSubscriptionUpdate, + ] = useMutation< + ArchiveUnArchiveSubscriptionMutation, + ArchiveUnArchiveSubscriptionMutationVariables + >( + UPDATE_SUBSCRIPTION, + { + onCompleted: (projectResponse) => { + const response = projectResponse?.private?.updateUserAlertSubscription; + if (!response) { + return; + } + if (response.ok) { + if (response.result?.isActive === true) { + alert.show( + strings.subscriptionUnarchived, + { variant: 'success' }, + ); + } else { + alert.show( + strings.subscriptionArchived, + { variant: 'success' }, + ); + refetch(); + } + } else { + alert.show( + strings.subscriptionFailedToUpdate, + { variant: 'danger' }, + ); + } + }, + onError: () => { + alert.show( + strings.subscriptionFailedToUpdate, + { variant: 'danger' }, + ); + }, + }, + ); + + const handleEditSubscription = useCallback((key: string) => { + setSelectedSubscription(key); + setShowSubscriptionModalTrue(); + }, [ + setShowSubscriptionModalTrue, + ]); + + const [ + triggerSubscriptionDelete, + ] = useMutation( + DELETE_SUBSCRIPTION, + { + onCompleted: (deleteResponse) => { + const response = deleteResponse?.private?.deleteUserAlertSubscription; + if (!response) { + return; + } + if (response.ok) { + alert.show( + strings.subscriptionDeleted, + { variant: 'success' }, + ); + } else { + alert.show( + strings.subscriptionFailedToDelete, + { variant: 'danger' }, + ); + } + }, + onError: () => { + alert.show( + strings.subscriptionFailedToDelete, + { variant: 'danger' }, + ); + }, + }, + ); + + const handleDeleteSubscription = useCallback((id: string) => { + triggerSubscriptionDelete({ + variables: { + subscriptionId: id, + }, + }).then(() => { + refetch(); + }); + }, [ + triggerSubscriptionDelete, + refetch, + ]); + + const handleTab = useCallback((newTab: TabKey) => { + setActiveTab(newTab); + setPage(1); + }, [ + setPage, + ]); + + const handleArchiveUnarchive = useCallback((id: string, archive: boolean) => { + const selectedSubscriptionDetails = data?.items.find( + (sub) => sub.id === id, + ); + + triggerSubscriptionUpdate({ + variables: { + subscriptionId: id, + data: { + isActive: archive, + filterAlertAdmin1s: selectedSubscriptionDetails?.filterAlertAdmin1s ?? [], + filterAlertCountry: selectedSubscriptionDetails?.filterAlertCountryId ?? '', + name: selectedSubscriptionDetails?.name ?? '', + }, + }, + }).then(() => { + refetch(); + }); + }, [ + data?.items, + triggerSubscriptionUpdate, + refetch, + ]); + + const activeRendererParams = useCallback(( + key: string, + value: UserAlertSubscriptionType, + ) => ({ + id: value.id, + name: value.name, + alertCount: value.totalAlertsCount ?? 0, + filterAlertUrgencies: value?.filterAlertUrgenciesDisplay, + filterAlertCertainties: value?.filterAlertCertaintiesDisplay, + filterAlertSeverities: value?.filterAlertSeveritiesDisplay, + filterAlertCategories: value?.filterAlertCategoriesDisplay, + filterAlertCountry: value?.filterAlertCountry.name, + filterAlertAdmin1s: value?.filterAlertAdmin1sDisplay?.map( + (admin) => admin.name, + ), + isActive: value?.isActive, + actions: handleArchiveUnarchive(value.id, false)} + onEditClick={() => handleEditSubscription(key)} + onSubscriptionRemove={() => handleDeleteSubscription(value.id)} + />, + }), [ + handleDeleteSubscription, + handleArchiveUnarchive, + handleEditSubscription, + ]); + + const archiveRendererParams = useCallback((_: string, value: UserAlertSubscriptionType) => ({ + id: value.id, + name: value.name, + alertCount: value.totalAlertsCount ?? 0, + filterAlertUrgencies: value?.filterAlertUrgenciesDisplay, + filterAlertCertainties: value?.filterAlertCertaintiesDisplay, + filterAlertSeverities: value?.filterAlertSeveritiesDisplay, + filterAlertCategories: value?.filterAlertCategoriesDisplay, + filterAlertCountry: value?.filterAlertCountry.name, + filterAlertAdmin1s: value?.filterAlertAdmin1sDisplay?.map( + (admin) => admin.name, + ), + isActive: value?.isActive, + actions: handleArchiveUnarchive(value.id, true)} + onSubscriptionRemove={() => handleDeleteSubscription(value.id)} + />, + }), [ + handleDeleteSubscription, + handleArchiveUnarchive, + ]); + + const selectedSubscriptionDetails = useMemo(() => { + const item = data?.items.find((sub) => sub.id === selectedSubscription); + if (!item) { + return undefined; + } + return ({ + ...item, + filterAlertCountry: item.filterAlertCountryId, + }); + }, [ + data, + selectedSubscription, + ]); + + const handleShowNewSubscriptionModal = useCallback(() => { + setSelectedSubscription(undefined); + setShowSubscriptionModalTrue(); + }, [ + setSelectedSubscription, + setShowSubscriptionModalTrue, + ]); + + return ( + + + )} + > + {strings.myNewSubscription} + + )} + footerActions={isDefined(data) && ( + + )} + pending={alertSubscriptionLoading} + errored={isDefined(alertSubscriptionError)} + overlayPending + > + {showSubscriptionModal && ( + + )} + + + + {strings.activeSubscriptionsTab} + + + {strings.archivedSubscriptionTab} + + + + + + + + + + + + + + ); +} +Component.displayName = 'MySubscriptions'; diff --git a/src/views/MySubscriptions/styles.module.css b/src/views/MySubscriptions/styles.module.css new file mode 100644 index 00000000..78857ec0 --- /dev/null +++ b/src/views/MySubscriptions/styles.module.css @@ -0,0 +1,5 @@ +.mySubscription { + .tabPanel { + display: contents; + } +} \ No newline at end of file diff --git a/src/views/NewSubscriptionModal/i18n.json b/src/views/NewSubscriptionModal/i18n.json new file mode 100644 index 00000000..a6e3ad5d --- /dev/null +++ b/src/views/NewSubscriptionModal/i18n.json @@ -0,0 +1,28 @@ +{ + "namespace": "mySubscriptionModal", + "strings": { + "createNewSubscription": "Create", + "sendViaEmailLabel": "Send via email", + "filterCountriesPlaceholder": "All Countries", + "filterAdmin1Placeholder": "All Admin1", + "filterUrgencyPlaceholder": "All Urgency Types", + "filterSeverityPlaceholder": "All Severity Types", + "filterCertaintyPlaceholder": "All Certainty Types", + "filterCategoryPlaceholder": "All Category Types", + "filterCategoryLabel": "Category", + "filterCountriesLabel": "Country", + "filterAdmin1Label": "Admin1", + "filterUrgencyLabel": "Urgency Level", + "filterSeverityLabel": "Severity Level", + "filterCertaintyLabel": "Certainty Level", + "filterRegionsLabel": "Regions", + "filterRegionsPlaceholder": "All Regions", + "newSubscriptionHeading": "New Subscription", + "newSubscriptionTitle": "Title", + "newSubscriptionCreatedSuccessfully": "Subscription created successfully.", + "newSubscriptionFailed": "Failed to create subscription", + "newSubscriptionLimitExceeded": "You have reached the maximum limit of 10 subscriptions", + "subscriptionUpdatedSuccessfully": "Subscription updated successfully.", + "failedToUpdateSubscription": "Failed to update subscription." + } +} diff --git a/src/views/NewSubscriptionModal/index.tsx b/src/views/NewSubscriptionModal/index.tsx new file mode 100644 index 00000000..3e3c833c --- /dev/null +++ b/src/views/NewSubscriptionModal/index.tsx @@ -0,0 +1,574 @@ +import { + useCallback, + useMemo, +} from 'react'; +import { + gql, + useMutation, + useQuery, +} from '@apollo/client'; +import { + Button, + Checkbox, + Modal, + MultiSelectInput, + RadioInput, + SelectInput, + TextInput, +} from '@ifrc-go/ui'; +import { useTranslation } from '@ifrc-go/ui/hooks'; +import { isNotDefined } from '@togglecorp/fujs'; +import { + createSubmitHandler, + getErrorObject, + getErrorString, + type ObjectSchema, + type PartialForm, + requiredCondition, + requiredStringCondition, + useForm, +} from '@togglecorp/toggle-form'; + +import { + AlertEnumsAndAllCountriesQuery, + AlertEnumsAndAllCountriesQueryVariables, + AlertEnumsQuery, + AppEnumCollection, + CreateUserAlertSubscriptionMutation, + CreateUserAlertSubscriptionMutationVariables, + FilteredAdminListQuery, + FilteredAdminListQueryVariables, + UpdateSubscriptionMutation, + UpdateSubscriptionMutationVariables, + UserAlertSubscriptionEmailFrequencyEnum, + UserAlertSubscriptionInput, +} from '#generated/types/graphql'; +import useAlert from '#hooks/useAlert'; +import { + stringIdSelector, + stringNameSelector, +} from '#utils/selectors'; + +import i18n from './i18n.json'; +import styles from './styles.module.css'; + +const ALERT_ENUMS_AND_ALL_COUNTIES = gql` +query AlertEnumsAndAllCountries { + enums { + AlertInfoCertainty { + key + label + } + AlertInfoUrgency { + key + label + } + AlertInfoSeverity { + key + label + } + AlertInfoCategory { + key + label + } + UserAlertSubscriptionEmailFrequency { + key + label + } + } + public { + id + allCountries { + name + id + } + } +} +`; + +const ADMIN_LIST = gql` +query FilteredAdminList($filters: Admin1Filter, $pagination: OffsetPaginationInput) { + public { + id + admin1s(filters: $filters, pagination: $pagination) { + items { + id + name + countryId + alertCount + } + } + } +} +`; + +const CREATE_USER_ALERT_SUBSCRIPTION = gql` +mutation CreateUserAlertSubscription( + $data: UserAlertSubscriptionInput!, + $filter: AlertFilter, +) { + private { + createUserAlertSubscription( + data: $data, + ) { + ok + errors + result { + id + name + notifyByEmail + isActive + filterAlertUrgencies + filterAlertSeverities + filterAlertCountryId + filterAlertCountry { + id + name + } + filterAlertCertainties + filterAlertCategories + filterAlertAdmin1sDisplay { + id + name + countryId + } + filterAlertAdmin1s + emailLastSentAt + emailFrequency + alerts (filters: $filter) { + count + } + } + } + id + } +} +`; + +const UPDATE_SUBSCRIPTION = gql` + mutation UpdateSubscription ( + $subscriptionId: ID!, + $data: UserAlertSubscriptionInput!, + ) { + private { + updateUserAlertSubscription( + id: $subscriptionId, + data: $data, + ) { + errors + ok + result { + id + name + notifyByEmail + isActive + filterAlertUrgencies + filterAlertSeverities + filterAlertCountryId + filterAlertCountry { + id + name + } + filterAlertCertainties + filterAlertCategories + filterAlertAdmin1sDisplay { + id + name + countryId + } + filterAlertAdmin1s + emailLastSentAt + emailFrequency + alerts { + count + } + } + } + } + } +`; + +type AdminOption = NonNullable['admin1s']>['items']>[number]; + +type Urgency = NonNullable[number]; +type Severity = NonNullable[number]; +type Certainty = NonNullable[number]; +type Category = NonNullable[number]; + +type EmailFrequency = NonNullable[number]; + +const adminKeySelector = (admin1: AdminOption) => admin1.id; +const urgencyKeySelector = (urgency: Urgency) => urgency.key; +const urgencyLabelSelector = (urgency: Urgency) => urgency.label; + +const severityKeySelector = (severity: Severity) => severity.key; +const severityLabelSelector = (severity: Severity) => severity.label; + +const certaintyKeySelector = (certainty: Certainty) => certainty.key; +const certaintyLabelSelector = (certainty: Certainty) => certainty.label; + +const frequencyKeySelector = (frequency: EmailFrequency) => frequency.key; +const frequencyLabelSelector = (frequency: EmailFrequency) => frequency.label; + +const categoryKeySelector = (category: Category) => category.key; +const categoryLabelSelector = (category: Category) => category.label; + +type PartialFormFields = PartialForm; + +type FormSchema = ObjectSchema; +const formSchema: FormSchema = { + fields: (value) => ({ + name: { + required: true, + requiredValidation: requiredStringCondition, + }, + filterAlertUrgencies: { + defaultValue: [], + }, + filterAlertCertainties: { + defaultValue: [], + }, + filterAlertSeverities: { + defaultValue: [], + }, + filterAlertCategories: { + defaultValue: [], + }, + filterAlertCountry: { + required: true, + }, + filterAlertAdmin1s: { + required: true, + }, + notifyByEmail: { + required: true, + }, + emailFrequency: { + required: !!value?.notifyByEmail, + requiredValidation: value?.notifyByEmail ? requiredCondition : undefined, + }, + }), +}; + +interface Props { + subscription?: { id?: string } & Partial; + onCloseModal: () => void; + onSuccess: (() => void) | undefined; +} + +function NewSubscriptionModal(props: Props) { + const { + subscription, + onCloseModal, + onSuccess, + } = props; + + const strings = useTranslation(i18n); + + const defaultFormValue = useMemo(() => ({ + id: subscription?.id, + name: subscription?.name, + filterAlertUrgencies: subscription?.filterAlertUrgencies + ?? [], + filterAlertCertainties: subscription?.filterAlertCertainties + ?? [], + filterAlertSeverities: subscription?.filterAlertSeverities + ?? [], + filterAlertCategories: subscription?.filterAlertCategories + ?? [], + filterAlertCountry: subscription?.filterAlertCountry, + filterAlertAdmin1s: subscription?.filterAlertAdmin1s + ?? [], + notifyByEmail: subscription?.notifyByEmail ?? false, + emailFrequency: subscription?.emailFrequency + ?? UserAlertSubscriptionEmailFrequencyEnum.Monthly, + }), [subscription]); + + const { + value, + setFieldValue, + error: formError, + setError, + validate, + } = useForm(formSchema, { value: defaultFormValue }); + + const fieldError = getErrorObject(formError); + + const alert = useAlert(); + + const [ + createAlertSubscription, + { loading: loadingSubscription }, + ] = useMutation< + CreateUserAlertSubscriptionMutation, + CreateUserAlertSubscriptionMutationVariables + >( + CREATE_USER_ALERT_SUBSCRIPTION, + { + onCompleted: (res) => { + const response = res.private.createUserAlertSubscription; + if (!response) { + return; + } + + if (response.ok) { + alert.show( + strings.newSubscriptionCreatedSuccessfully, + { variant: 'success' }, + ); + onCloseModal(); + if (onSuccess) { + onSuccess(); + } + } else { + const errorMessages = response?.errors + ?.map((error: { messages: string; }) => error.messages) + .filter((message: string) => message) + .join(', '); + alert.show(errorMessages, { variant: 'danger' }); + } + }, + onError: () => { + alert.show( + strings.newSubscriptionFailed, + { variant: 'danger' }, + ); + }, + }, + ); + + const [ + triggerSubscriptionUpdate, + ] = useMutation( + UPDATE_SUBSCRIPTION, + { + onCompleted: (projectResponse) => { + const response = projectResponse?.private?.updateUserAlertSubscription; + if (!response) { + return; + } + if (response.ok) { + alert.show( + strings.subscriptionUpdatedSuccessfully, + { variant: 'success' }, + ); + onCloseModal(); + if (onSuccess) { + onSuccess(); + } + } else { + const errorMessages = response?.errors + ?.map((error: { messages: string; }) => error.messages) + .filter((message: string) => message) + .join(', '); + alert.show(errorMessages, { variant: 'danger' }); + } + }, + onError: () => { + alert.show( + strings.failedToUpdateSubscription, + { variant: 'danger' }, + ); + }, + }, + ); + + const { + data: alertEnumsResponse, + } = useQuery( + ALERT_ENUMS_AND_ALL_COUNTIES, + ); + + const adminQueryVariables = useMemo( + () => { + if (isNotDefined(value.filterAlertCountry)) { + return { + filters: undefined, + // FIXME: Implement search select input + pagination: { + offset: 0, + limit: 500, + }, + }; + } + + return { + filters: { + country: { pk: value.filterAlertCountry }, + }, + // FIXME: Implement search select input + pagination: { + offset: 0, + limit: 500, + }, + }; + }, + [value.filterAlertCountry], + ); + + const { + data: adminResponse, + } = useQuery( + ADMIN_LIST, + { variables: adminQueryVariables, skip: isNotDefined(value.filterAlertCountry) }, + ); + + const subscriptionCreate = useCallback(() => { + const handler = createSubmitHandler( + validate, + setError, + (val) => { + if (subscription?.id) { + triggerSubscriptionUpdate({ + variables: { + subscriptionId: subscription.id, + data: val as UserAlertSubscriptionInput, + }, + }).then(() => { + if (onSuccess) { + onSuccess(); + } + }); + } else { + createAlertSubscription({ + variables: { + data: { + ...val as UserAlertSubscriptionInput, + isActive: true, + }, + }, + }); + } + }, + ); + handler(); + }, [ + setError, + subscription?.id, + triggerSubscriptionUpdate, + createAlertSubscription, + validate, + onSuccess, + ]); + + const handleFormSubmit = createSubmitHandler(validate, setError, subscriptionCreate); + + return ( + + {strings.createNewSubscription} + + )} + footerContentClassName={styles.createButton} + contentViewType="vertical" + spacing="comfortable" + onClose={onCloseModal} + > + +
+ + + + + + +
+ + +
+ ); +} + +export default NewSubscriptionModal; diff --git a/src/views/NewSubscriptionModal/styles.module.css b/src/views/NewSubscriptionModal/styles.module.css new file mode 100644 index 00000000..7c566bd5 --- /dev/null +++ b/src/views/NewSubscriptionModal/styles.module.css @@ -0,0 +1,13 @@ +.subscription-modal { + .create-button { + display: flex; + flex-direction: column; + align-items: flex-start; + } + + .filters { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr)); + gap: var(--go-ui-spacing-md); + } +} \ No newline at end of file diff --git a/src/views/RecoverAccount/i18n.json b/src/views/RecoverAccount/i18n.json new file mode 100644 index 00000000..abf4ff15 --- /dev/null +++ b/src/views/RecoverAccount/i18n.json @@ -0,0 +1,12 @@ +{ + "namespace": "recoverAccount", + "strings": { + "pageTitle": "IFRC Alert-Hub - Recover Account", + "pageHeading": "Recover Account", + "pageDescription": "Enter the email you used during registration", + "emailInputLabel": "Email", + "submitButtonLabel": "Submit recovery request", + "failureMessageTitle": "Could not recover account!", + "successfulMessage":" Account recovery submitted! Please check your email for further action!" + } +} diff --git a/src/views/RecoverAccount/index.tsx b/src/views/RecoverAccount/index.tsx new file mode 100644 index 00000000..bcba31ec --- /dev/null +++ b/src/views/RecoverAccount/index.tsx @@ -0,0 +1,189 @@ +import { + useMemo, + useState, +} from 'react'; +import { + gql, + useMutation, +} from '@apollo/client'; +import { CheckboxFillIcon } from '@ifrc-go/icons'; +import { + Button, + Message, + TextInput, +} from '@ifrc-go/ui'; +import { useTranslation } from '@ifrc-go/ui/hooks'; +import { + createSubmitHandler, + getErrorObject, + nonFieldError, + ObjectSchema, + requiredStringCondition, + useForm, +} from '@togglecorp/toggle-form'; + +import HCaptcha from '#components/Captcha'; +import NonFieldError from '#components/NonFiledError'; +import Page from '#components/Page'; +import { + PasswordResetTriggerMutation, + PasswordResetTriggerMutationVariables, + UserPasswordResetTriggerInput, +} from '#generated/types/graphql'; +import useAlert from '#hooks/useAlert'; +import { transformToFormError } from '#utils/errorTransform'; + +import i18n from './i18n.json'; +import styles from './styles.module.css'; + +interface FormFields { + email?: string; + captcha?: string; +} + +const PASSWORD_RECOVERY_MUTATION = gql` + mutation passwordResetTrigger($data: UserPasswordResetTriggerInput!) { + public { + passwordResetTrigger(data: $data) { + ok + errors + } + } + } +`; +type FormType = Partial; +type FormSchema = ObjectSchema; +type FormSchemaFields = ReturnType; + +const formSchema: ObjectSchema = { + fields: (): FormSchemaFields => ({ + email: { + required: true, + requiredValidation: requiredStringCondition, + }, + captcha: { + required: true, + requiredValidation: requiredStringCondition, + }, + }), +}; + +// eslint-disable-next-line import/prefer-default-export +export function Component() { + const strings = useTranslation(i18n); + const alert = useAlert(); + const [isSubmitted, setIsSubmitted] = useState(false); + + const defaultFormValue: FormFields = {}; + const { + value: formValue, + error: formError, + setFieldValue, + setError, + validate, + } = useForm(formSchema, { value: defaultFormValue }); + + const [ + requestPasswordRecovery, + { loading }, + ] = useMutation( + PASSWORD_RECOVERY_MUTATION, + { + onCompleted: (response) => { + const { + public: { passwordResetTrigger }, + } = response; + if (passwordResetTrigger?.ok) { + setIsSubmitted(true); + } else if (passwordResetTrigger.errors) { + const formErrors = transformToFormError(passwordResetTrigger.errors); + setError(formErrors); + alert.show( + strings.failureMessageTitle, + { variant: 'danger' }, + ); + } + }, + onError: (error) => { + setError({ [nonFieldError]: error.message }); + alert.show( + strings.failureMessageTitle, + { variant: 'danger' }, + ); + }, + }, + ); + + const handleFormSubmit = useMemo( + () => createSubmitHandler( + validate, + setError, + (formValues: FormFields) => { + requestPasswordRecovery({ + variables: { + data: formValues as UserPasswordResetTriggerInput, + }, + }); + }, + ), + [validate, setError, requestPasswordRecovery], + ); + + const fieldError = getErrorObject(formError); + + if (isSubmitted) { + return ( + + } + title={strings.successfulMessage} + /> + + + ); + } + + return ( + +
+ + +
+ + +
+ +
+ ); +} + +Component.displayName = 'RecoverAccount'; diff --git a/src/views/RecoverAccount/styles.module.css b/src/views/RecoverAccount/styles.module.css new file mode 100644 index 00000000..ed47a26b --- /dev/null +++ b/src/views/RecoverAccount/styles.module.css @@ -0,0 +1,21 @@ +.recover-account { + .form { + display: flex; + flex-direction: column; + flex-grow: 1; + gap: var(--go-ui-spacing-lg); + margin: 0 auto; + max-width: var(--go-ui-width-content-max); + + .submit-button { + align-self: center; + } + + .actions { + display: flex; + flex-direction: column; + gap: var(--go-ui-spacing-lg); + align-items: center; + } + } +} diff --git a/src/views/RecoverAccountConfirm/i18n.json b/src/views/RecoverAccountConfirm/i18n.json new file mode 100644 index 00000000..0cb633fb --- /dev/null +++ b/src/views/RecoverAccountConfirm/i18n.json @@ -0,0 +1,16 @@ +{ + "namespace": "recoverAccountConfirm", + "strings": { + "pageTitle": "IFRC GO - Recover Password", + "pageHeading": "Recover Account", + "newPassword": "New Password", + "confirmPassword": "Confirm new Password", + "submitButtonLabel": "Submit", + "successfulMessageTitle": "Password Changed!", + "successfulMessageDescription": "Your password has been changed. Please login again!", + "failureMessageTitle": "Could not change password!", + "tokenMissingMessage": "Token is missing", + "uuidMissingMessage":"Uuid is missing", + "incompleteFormMessage": "Please fill in all the required fields." + } +} diff --git a/src/views/RecoverAccountConfirm/index.tsx b/src/views/RecoverAccountConfirm/index.tsx new file mode 100644 index 00000000..aac8504f --- /dev/null +++ b/src/views/RecoverAccountConfirm/index.tsx @@ -0,0 +1,239 @@ +import { useCallback } from 'react'; +import { + useNavigate, + useParams, +} from 'react-router-dom'; +import { + gql, + useMutation, +} from '@apollo/client'; +import { + Button, + PasswordInput, +} from '@ifrc-go/ui'; +import { useTranslation } from '@ifrc-go/ui/hooks'; +import { isTruthyString } from '@togglecorp/fujs'; +import { + addCondition, + createSubmitHandler, + getErrorObject, + type ObjectSchema, + requiredStringCondition, + undefinedValue, + useForm, +} from '@togglecorp/toggle-form'; + +import HCaptcha from '#components/Captcha'; +import NonFieldError from '#components/NonFiledError'; +import Page from '#components/Page'; +import { + PasswordResetConfirmMutation, + PasswordResetConfirmMutationVariables, + UserPasswordResetConfirmInput, +} from '#generated/types/graphql'; +import useAlert from '#hooks/useAlert'; +import { transformToFormError } from '#utils/errorTransform'; + +import i18n from './i18n.json'; +import styles from './styles.module.css'; + +interface FormFields { + newPassword?: string; + confirmPassword?: string; + captcha?: string; +} + +const PASSWORD_RESET_CONFIRM_MUTATION = gql` + mutation PasswordResetConfirm($data: UserPasswordResetConfirmInput!) { + public { + passwordResetConfirm(data: $data) { + errors + ok + } + } + } +`; + +type FormType = Partial; +type FormSchema = ObjectSchema; +type FormSchemaFields = ReturnType; + +function getPasswordMatchCondition(referenceVal: string | undefined) { + return (val: string | undefined) => { + if (isTruthyString(val) && isTruthyString(referenceVal) && val !== referenceVal) { + return 'Passwords do not match'; + } + return undefined; + }; +} + +const formSchema: FormSchema = { + fields: (value): FormSchemaFields => { + let baseSchema = { + newPassword: { + required: true, + requiredValidation: requiredStringCondition, + }, + confirmPassword: { + required: true, + requiredValidation: requiredStringCondition, + }, + captcha: { + required: true, + requiredValidation: requiredStringCondition, + }, + } as FormSchemaFields; + + baseSchema = addCondition( + baseSchema, + value, + ['newPassword'], + ['confirmPassword'], + (val) => ({ + confirmPassword: { + required: true, + requiredValidation: requiredStringCondition, + forceValue: undefinedValue, + validations: [getPasswordMatchCondition(val?.newPassword)], + }, + }), + ); + + return baseSchema; + }, +}; + +const defaultFormValue: FormFields = {}; + +// eslint-disable-next-line import/prefer-default-export +export function Component() { + const { userId, resetToken } = useParams<{ userId?: string, resetToken?: string }>(); + const strings = useTranslation(i18n); + const navigate = useNavigate(); + const alert = useAlert(); + const { + value: formValue, + error: formError, + setFieldValue, + setError, + validate, + } = useForm(formSchema, { value: defaultFormValue }); + + const [passwordResetConfirm, { loading }] = useMutation< + PasswordResetConfirmMutation, + PasswordResetConfirmMutationVariables + >(PASSWORD_RESET_CONFIRM_MUTATION, { + onCompleted: (data) => { + if (data.public.passwordResetConfirm.ok) { + alert.show( + strings.successfulMessageTitle, + { + description: strings.successfulMessageDescription, + variant: 'success', + }, + ); + navigate('/login'); + } else { + setError(transformToFormError( + data.public.passwordResetConfirm.errors, + )); + alert.show( + strings.failureMessageTitle, + { variant: 'danger' }, + ); + } + }, + onError: () => { + alert.show( + strings.failureMessageTitle, + { variant: 'danger' }, + ); + }, + }); + const handleChangePassword = useCallback( + (formValues: FormFields) => { + if (!userId) { + alert.show( + strings.uuidMissingMessage, + { variant: 'warning' }, + ); + return; + } + if (!resetToken) { + alert.show( + strings.tokenMissingMessage, + { variant: 'warning' }, + ); + return; + } + passwordResetConfirm({ + variables: { + data: { + newPassword: formValues.newPassword, + captcha: formValues.captcha, + token: resetToken, + uuid: userId, + } as UserPasswordResetConfirmInput, + }, + }); + }, + [passwordResetConfirm, userId, resetToken, alert, strings], + ); + + const handleFormSubmit = createSubmitHandler(validate, setError, handleChangePassword); + + const fieldError = getErrorObject(formError); + + return ( + +
+ + + +
+ + +
+ +
+ ); +} + +Component.displayName = 'RecoverAccountConfirm'; diff --git a/src/views/RecoverAccountConfirm/styles.module.css b/src/views/RecoverAccountConfirm/styles.module.css new file mode 100644 index 00000000..2a3bf797 --- /dev/null +++ b/src/views/RecoverAccountConfirm/styles.module.css @@ -0,0 +1,22 @@ +.recover-account-confirm { + .form { + display: flex; + flex-direction: column; + flex-grow: 1; + gap: var(--go-ui-spacing-lg); + margin: 0 auto; + max-width: var(--go-ui-width-content-max); + + + .actions { + display: flex; + flex-direction: column; + gap: var(--go-ui-spacing-md); + align-items: center; + + .submit-button { + align-self: center; + } + } + } +} diff --git a/src/views/Register/i18n.json b/src/views/Register/i18n.json new file mode 100644 index 00000000..5d3b31b5 --- /dev/null +++ b/src/views/Register/i18n.json @@ -0,0 +1,22 @@ +{ + "namespace": "register", + "strings": { + "registerTitle": "IFRC Alert-Hub - Register", + "registerHeader": "Register", + "registerSubHeader": "The IFRC Alert Hub platform is designed for staff, members, and volunteers of the Red Cross Red Crescent Movement (National Societies, the IFRC and the ICRC). Please register for a user account to access information for logged in users. Other responders and members of the public may browse the public areas of the site without registering for an account.", + "registerFirstName": "First Name", + "registerLastName": "Last Name", + "registerEmail": "Email", + "registerCountry": "Country", + "registerCity": "City", + "registerOrganizationType": "Organization Type", + "registerOrganizationName": "Organization Name", + "registerPassword": "Password", + "registerConfirmPassword": "Confirm Password", + "registerSubmit": "Register", + "registerAccountPresent": "Already have an account? {loginLink}", + "registerLogin": "Login", + "registrationSuccess": "Successfully created a user!", + "registrationFailure": "Sorry could not register new user right now!" + } +} diff --git a/src/views/Register/index.tsx b/src/views/Register/index.tsx new file mode 100644 index 00000000..d196a229 --- /dev/null +++ b/src/views/Register/index.tsx @@ -0,0 +1,272 @@ +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { + gql, + useMutation, +} from '@apollo/client'; +import { + Button, + TextInput, +} from '@ifrc-go/ui'; +import { useTranslation } from '@ifrc-go/ui/hooks'; +import { resolveToComponent } from '@ifrc-go/ui/utils'; +import { isTruthyString } from '@togglecorp/fujs'; +import { + addCondition, + createSubmitHandler, + emailCondition, + getErrorObject, + nonFieldError, + type ObjectSchema, + removeNull, + requiredStringCondition, + undefinedValue, + useForm, +} from '@togglecorp/toggle-form'; + +import HCaptcha from '#components/Captcha'; +import Link from '#components/Link'; +import NonFieldError from '#components/NonFiledError'; +import Page from '#components/Page'; +import { + RegisterMutation, + RegisterMutationVariables, + UserRegisterInput, +} from '#generated/types/graphql'; +import useAlert from '#hooks/useAlert'; +import { transformToFormError } from '#utils/errorTransform'; + +import i18n from './i18n.json'; +import styles from './styles.module.css'; + +const REGISTER_MUTATION = gql` + mutation Register($data: UserRegisterInput!) { + public { + register(data: $data){ + errors + ok + } + } + } +`; + +function getPasswordMatchCondition(referenceVal: string | undefined) { + function passwordMatchCondition(val: string | undefined) { + if (isTruthyString(val) && isTruthyString(referenceVal) && val !== referenceVal) { + return 'Passwords do not match'; + } + return undefined; + } + + return passwordMatchCondition; +} + +type PartialFormFields = Partial; +type FormSchema = ObjectSchema; +type FormSchemaFields = ReturnType; + +const formSchema: FormSchema = { + fields: (value): FormSchemaFields => { + let fields: FormSchemaFields = { + firstName: { + required: true, + requiredValidation: requiredStringCondition, + }, + lastName: { + required: true, + requiredValidation: requiredStringCondition, + }, + email: { + required: true, + requiredValidation: requiredStringCondition, + validations: [emailCondition], + }, + password: { + required: true, + requiredValidation: requiredStringCondition, + }, + captcha: { + required: true, + requiredValidation: requiredStringCondition, + }, + }; + fields = addCondition( + fields, + value, + ['password'], + ['confirmPassword'], + (val) => ({ + confirmPassword: { + required: true, + requiredValidation: requiredStringCondition, + forceValue: undefinedValue, + validations: [getPasswordMatchCondition(val?.password)], + }, + }), + ); + + return fields; + }, +}; + +// eslint-disable-next-line import/prefer-default-export +export function Component() { + const strings = useTranslation(i18n); + const alert = useAlert(); + const navigate = useNavigate(); + const [formValue] = useState({}); + const { + value, + setFieldValue, + setError, + validate, + error: fieldError, + } = useForm(formSchema, { + value: formValue, + }); + + const error = getErrorObject(fieldError); + const [ + triggerRegister, + { + loading: registerPending, + }, + ] = useMutation(REGISTER_MUTATION, { + onCompleted: (response) => { + const { public: publicRes } = response; + if (!publicRes) { + return; + } + const { register: registerRes } = publicRes; + if (!registerRes) { + return; + } + const { errors, ok } = registerRes; + + if (errors) { + const formError = transformToFormError(removeNull( + errors, + )); + setError(formError); + } else if (ok) { + navigate('/login'); + alert.show( + strings.registrationSuccess, + { variant: 'success' }, + ); + } + }, + onError: (errors) => { + setError({ + [nonFieldError]: errors.message, + }); + alert.show( + strings.registrationFailure, + { variant: 'danger' }, + ); + }, + }); + + const handleFormSubmit = createSubmitHandler( + validate, + setError, + (finalValue) => { + const val = finalValue as UserRegisterInput; + triggerRegister({ + variables: { + data: { + captcha: val.captcha, + email: val.email, + firstName: val.firstName, + lastName: val.lastName, + password: val.password, + }, + }, + }); + }, + ); + const loginInfo = resolveToComponent(strings.registerAccountPresent, { + loginLink: ( + + {strings.registerLogin} + + ), + }); + + return ( + +
+ + + + + + +
+
+ + +
+ {loginInfo} +
+
+
+ ); +} + +Component.displayName = 'Register'; diff --git a/src/views/Register/styles.module.css b/src/views/Register/styles.module.css new file mode 100644 index 00000000..a4a53686 --- /dev/null +++ b/src/views/Register/styles.module.css @@ -0,0 +1,30 @@ +.register { + .main-section { + display: flex; + flex-direction: column; + gap: var(--go-ui-spacing-2xl); + align-items: center; + + .form { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); + grid-gap: var(--go-ui-spacing-lg); + background-color: var(--go-ui-color-white); + width: 100%; + max-width: var(--go-ui-width-content-max); + } + + .actions { + display: flex; + flex-direction: column; + gap: var(--go-ui-spacing-md); + align-items: center; + + .login { + display: flex; + gap: var(--go-ui-spacing-xs); + align-items: center; + } + } + } +} diff --git a/src/views/Resources/i18n.json b/src/views/Resources/i18n.json index 7f6890f5..0046c119 100644 --- a/src/views/Resources/i18n.json +++ b/src/views/Resources/i18n.json @@ -2,18 +2,28 @@ "namespace": "resources", "strings": { "resourceHeadingTitle": "Resources", - "resourceAlerthubTitle":"IFRC Alert Hub - Resources", - "resourceHeadingDescription":"Alert Hub is an open source web project developed in collaboration with the International Federation of Red Cross and Red Crescent Societies (IFRC) as a part of the University College London (UCL) Industry Exchange Network (IXN). You can find all the source code and instructions under the following repositories.", - "resourceAlertHubAPIs":"Alert Hub APIs", - "resourceAlertHubAPIsDescription":"Alert Hub APIs enable third-party developers and rebroadcasters to access alerts and alert feed.", - "resourceLearMore":"Learn More", - "resourceAlertHubFrontendTitle":"Alert Hub Frontend", - "resourceAlertHubFrontendDescription":"This repository contains the frontend code for the IFRC Alert Hub. The goal of the IFRC Alert Hub is to ensure that communities everywhere receive the most timely and effective emergency alerting possible, and can thereby take action to safeguard their lives and livelihoods.", - "resourceAlertHubCapAggregatorTitle":"Alert Hub CAP Aggregator", - "resourceAlertHubCapAggregatorDescription":"The CAP Aggregator is an alert aggregation service built for IFRC's Alert Hub. Public alerts use the Common Alerting Protocol (CAP) Version 1.2 standard.", - "resourceAlertHubAlertManagerTitle":"Alert Hub Alert Manager", - "resourceAlertHubAlertManagerDescription":"The Alert Manager is an alert distribution service built for IFRC's Alert Hub. Public alerts use the Common Alerting Protocol (CAP) Version 1.2 standard.", - "resourceAlertHubSubscriptionSystemTitle":"Alert Hub Subscription System", - "resourceAlertHubSubscriptionSystemDescription":"This project serves as a back-end application of IFRC Alert Hub designed to work with IFRC/UCL Alert Hub CAP Aggregator. This application relies on it to get real-time updates about alerts." - } + "resourceAlerthubTitle": "IFRC Alert Hub - Resources", + "resourceHeadingDescription": "Alert Hub is an open source web project developed in collaboration with the International Federation of Red Cross and Red Crescent Societies (IFRC) as a part of the University College London (UCL) Industry Exchange Network (IXN). You can find all the source code and instructions under the following repositories.", + "earlyWarningResourcesTitle": "Early Warning Resources", + "earlyWarningResourceIfrcewea":"IFRC Early Warning, Early Action", + "earlyWarningResourceGdpcEws": "GDPC Early Warning Systems", + "earlyWarningResourceWmoCapCourse": "WMO Common Alerting Protocol Course", + "earlyWarningResourceIfrcPape": "IFRC PAPE Messages", + "faqSectionTitle": "FAQ", + "faqWhoIsTheAudience": "Who is the audience of the IFRC AlertHub?", + "faqWhoIsBehind": "Who is behind it?", + "faqHowToAccess": "How to access the API?", + "howToSubscribeTitle": "How to Subscribe?", + "ifrcRelatedLinksTitle": "IFRC related links:", + "ifrcRelatedLinksGo": "GO", + "ifrcRelatedLinksGdpc": "GDPC", + "ifrcRelatedLinksClimateCenter": "Climate Centre", + "ifrcRelatedLinksAnticipationHub": "Anticipation Hub", + "ifrcRelatedExternalLinksTitle": "External links:", + "ifrcRelatedExternalLinksGo": "WMO register of alerting authorities", + "ifrcRelatedExternalLinksCapImplementation": "CAP implementation workshop", + "ifrcRelatedExternalLinksGooglePublicAlerts": "Google Public Alerts", + "ifrcRelatedExternalLinksEarlyWarningAllInitiative":"Early Warning For All Initiative", + "ifrcSubscriptionDescription": "To subscribe to alerts on the IFRC AlertHub platform, log in to your account and navigate to the My Subscriptions section located in the top-left corner of the site. Choose the topics or categories that interest you, such as specific countries, hazards, or urgency levels. Click the Subscribe button, set your preferred alert frequency (e.g., daily or weekly), and save your preferences. Once subscribed, you’ll receive updates tailored to your selections. You can create multiple subscriptions to cover different interests. To modify or cancel your subscriptions, simply return to the My Subscriptions section." + } } \ No newline at end of file diff --git a/src/views/Resources/index.tsx b/src/views/Resources/index.tsx index c646e482..b5966e05 100644 --- a/src/views/Resources/index.tsx +++ b/src/views/Resources/index.tsx @@ -1,3 +1,4 @@ +import { ExternalLinkLineIcon } from '@ifrc-go/icons'; import { Container } from '@ifrc-go/ui'; import { useTranslation } from '@ifrc-go/ui/hooks'; @@ -11,72 +12,162 @@ import styles from './styles.module.css'; export function Component() { const strings = useTranslation(i18n); - const resourceData = [ + const earlyWarningResources = [ { id: 1, - heading: strings.resourceAlertHubAPIs, - description: strings.resourceAlertHubAPIsDescription, + title: strings.earlyWarningResourceIfrcewea, + url: 'https://www.ifrc.org/our-work/disasters-climate-and-crises/climate-smart-disaster-risk-reduction/early-warning-early', + }, + { + id: 2, + title: strings.earlyWarningResourceGdpcEws, + url: 'https://preparecenter.org/topic/early-warning-systems/', + }, + { + id: 3, + title: strings.earlyWarningResourceWmoCapCourse, + url: 'https://etrp.wmo.int/course/view.php?id=157', + }, + { + id: 4, + title: strings.earlyWarningResourceIfrcPape, + url: 'https://www.ifrc.org/our-work/disasters-climate-and-crises/climate-smart-disaster-risk-reduction/PAPE', + }, + ]; + const frequentlyAskedQuestion = [ + { + id: 1, + title: strings.faqWhoIsTheAudience, + url: 'https://alerthub.ifrc.org/about', + }, + { + id: 2, + title: strings.faqWhoIsBehind, + url: 'https://alerthub.ifrc.org/feeds', + }, + { + id: 3, + title: strings.faqHowToAccess, url: 'https://github.com/IFRCGo/alert-hub-web-app/blob/develop/APIDOCS.md', }, + + ]; + const ifrcResources = [ + { + id: 1, + title: strings.ifrcRelatedLinksGo, + url: 'https://go.ifrc.org/', + }, { id: 2, - heading: strings.resourceAlertHubFrontendTitle, - description: strings.resourceAlertHubFrontendDescription, - url: 'https://github.com/IFRCGo/alert-hub-web-app#readme', + title: strings.ifrcRelatedLinksGdpc, + url: 'https://preparecenter.org/', }, { id: 3, - heading: strings.resourceAlertHubCapAggregatorTitle, - description: strings.resourceAlertHubCapAggregatorDescription, - url: 'https://github.com/IFRC-Alert-Hub/Alert-Hub-CAP-Aggregator#readme', + title: strings.ifrcRelatedLinksClimateCenter, + url: 'https://www.climatecentre.org/', }, { id: 4, - heading: strings.resourceAlertHubAlertManagerTitle, - description: strings.resourceAlertHubAlertManagerDescription, - url: 'https://github.com/IFRC-Alert-Hub/Alert-Hub-Alert-Manager#readme', + title: strings.ifrcRelatedLinksAnticipationHub, + url: 'https://www.anticipation-hub.org/', + }, + ]; + const externalResources = [ + { + id: 1, + title: strings.ifrcRelatedExternalLinksGo, + url: 'https://alertingauthority.wmo.int', + }, + { + id: 2, + title: strings.ifrcRelatedExternalLinksCapImplementation, + url: 'https://cap-workshop.alert-hub.org/2023/index.html', + }, + { + id: 3, + title: strings.ifrcRelatedExternalLinksGooglePublicAlerts, + url: 'https://support.google.com/publicalerts/?hl=en', }, { - id: 5, - heading: strings.resourceAlertHubSubscriptionSystemTitle, - description: strings.resourceAlertHubSubscriptionSystemDescription, - url: 'https://github.com/IFRC-Alert-Hub/Alert-Hub-Subscription-System#readme', + id: 4, + title: strings.ifrcRelatedExternalLinksEarlyWarningAllInitiative, + url: 'https://www.un.org/en/climatechange/early-warnings-for-all', }, ]; return ( - {resourceData.map( - (resource) => ( - - {strings.resourceLearMore} - - )} - > - {resource.description} - - ), - )} + {earlyWarningResources.map((resource) => ( + } + external + > + {resource.title} + + ))} + + + {frequentlyAskedQuestion.map((faq) => ( + } + external + > + {faq.title} + + ))} + + + + {ifrcResources.map((externalLink) => ( + } + external + > + {externalLink.title} + + ))} + + + + {externalResources.map((externalLink) => ( + } + external + > + {externalLink.title} + + ))} ); diff --git a/src/views/Resources/styles.module.css b/src/views/Resources/styles.module.css index 001a5256..21876c86 100644 --- a/src/views/Resources/styles.module.css +++ b/src/views/Resources/styles.module.css @@ -1,21 +1,16 @@ .resources { - .resources-card { - display: flex; - flex-direction: column; - gap: var(--go-ui-spacing-sm); - border-radius: var(--go-ui-border-radius-lg); - box-shadow: var(--go-ui-box-shadow-md); - padding: var(--go-ui-spacing-md); - - .resources-item { - text-decoration: none; - color: var(--go-ui-color-primary-red); - font-weight: var(--go-ui-font-weight-medium); - } + display: flex; + flex-direction: column; + gap: var(--go-ui-spacing-lg); - .resources-item:hover { - text-decoration: underline; - color: var(--go-ui-color-primary-red); - } + .resource-link{ + display: flex; + padding-bottom: var(--go-ui-spacing-md); + text-decoration: none; + font-weight: var(--go-ui-font-weight-normal); + } + .resource-link:hover { + text-decoration: underline; + color: var(--go-ui-color-red-hover); } } diff --git a/src/views/RootLayout/i18n.json b/src/views/RootLayout/i18n.json new file mode 100644 index 00000000..c084d307 --- /dev/null +++ b/src/views/RootLayout/i18n.json @@ -0,0 +1,8 @@ +{ + "namespace": "root", + "strings": { + "cookiesBannerDescription": "By entering this website, you consent to the use of technologies, such as cookies and analytics. These will be used to analyse traffic to the website, allowing us to understand visitor preferences and improve our services", + "cookiesBannerLearnMore": "Learn more", + "cookiesBannerIAccept": "I Accept" + } +} diff --git a/src/views/RootLayout/index.tsx b/src/views/RootLayout/index.tsx index 004241d0..722cda08 100644 --- a/src/views/RootLayout/index.tsx +++ b/src/views/RootLayout/index.tsx @@ -8,11 +8,21 @@ import { Outlet, useNavigation, } from 'react-router-dom'; -import { AlertContainer } from '@ifrc-go/ui'; +import { AlertInformationLineIcon } from '@ifrc-go/icons'; +import { + AlertContainer, + Button, + Container, + PageContainer, +} from '@ifrc-go/ui'; import { Language, LanguageContext, } from '@ifrc-go/ui/contexts'; +import { + useBooleanState, + useTranslation, +} from '@ifrc-go/ui/hooks'; import { _cs, listToGroupList, @@ -21,13 +31,16 @@ import { } from '@togglecorp/fujs'; import GlobalFooter from '#components/GlobalFooter'; +import Link from '#components/Link'; import Navbar from '#components/Navbar'; import useDebouncedValue from '#hooks/useDebouncedValue'; +import i18n from './i18n.json'; import styles from './styles.module.css'; // eslint-disable-next-line import/prefer-default-export export function Component() { + const strings = useTranslation(i18n); const { state } = useNavigation(); const isLoading = state === 'loading'; const isLoadingDebounced = useDebouncedValue(isLoading); @@ -38,6 +51,17 @@ export function Component() { setStrings, } = useContext(LanguageContext); + // FIXME: To be made functional after the implications of cookie rejections are finalized + const [ + isCookiesBannerVisible, + { setFalse: hideCookiesBanner }, + ] = useBooleanState(false); + + const handleClick = useCallback(() => { + // FIXME: Add cookies permission to session storage + hideCookiesBanner(); + }, [hideCookiesBanner]); + const fetchLanguage = useCallback(async (lang: Language) => { setLanguagePending(true); const resource = await import(`./translations/${lang}.json`); @@ -103,6 +127,41 @@ export function Component() { className={styles.footer} /> + {isCookiesBannerVisible && ( +
+ {isCookiesBannerVisible && ( + + + )} + spacing="comfortable" + actions={( + <> + + {strings.cookiesBannerLearnMore} + + + + )} + /> + + )} +
+ )} ); } diff --git a/src/views/RootLayout/styles.module.css b/src/views/RootLayout/styles.module.css index faa02891..cbd7132c 100644 --- a/src/views/RootLayout/styles.module.css +++ b/src/views/RootLayout/styles.module.css @@ -4,6 +4,16 @@ flex-direction: column; height: 100vh; + .cookies-banner { + background-color: var(--go-ui-color-primary-blue); + width: 100%; + color: var(--go-ui-color-white); + + .alert-info-icon { + font-size: 3rem; + } + } + .navigation-loader { position: fixed; transition: var(--go-ui-duration-animation-medium) width ease-out; diff --git a/tsconfig.json b/tsconfig.json index 50236aba..58268f93 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,6 @@ "#views/*": ["./src/views/*"], "#routes": ["./src/App/routes"] }, - "target": "ESNext", "lib": ["DOM", "DOM.Iterable", "ESNext"], "module": "ESNext", diff --git a/vite.config.ts b/vite.config.ts index 97d5b483..45403fd5 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -9,6 +9,7 @@ import { ValidateEnv as validateEnv } from '@julr/vite-plugin-validate-env'; import { VitePluginRadar } from 'vite-plugin-radar'; import alertHubPackage from './package.json'; +import envConfig from './env'; /* Get commit hash */ const commitHash = execSync('git rev-parse --short HEAD').toString(); @@ -35,7 +36,7 @@ export default defineConfig(({ mode }) => { reactSwc(), tsconfigPaths(), webfontDownload(), - validateEnv(), + validateEnv(envConfig), isProd ? compression() : undefined, VitePluginRadar({ analytics: {