From b3d4a4a2240a14406ce87997c1eb6585958100ec Mon Sep 17 00:00:00 2001 From: jeafreezy Date: Thu, 13 Feb 2025 10:08:49 +0100 Subject: [PATCH 01/12] prod fix --- frontend/src/app/providers/auth-provider.tsx | 2 +- frontend/src/utils/number-utils.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/providers/auth-provider.tsx b/frontend/src/app/providers/auth-provider.tsx index bbf40b65..f54e97d1 100644 --- a/frontend/src/app/providers/auth-provider.tsx +++ b/frontend/src/app/providers/auth-provider.tsx @@ -3,7 +3,7 @@ import React, { useContext, useEffect, useState - } from 'react'; +} from 'react'; import { apiClient } from '@/services/api-client'; import { authService } from '@/services'; import { showErrorToast, showSuccessToast } from '@/utils'; diff --git a/frontend/src/utils/number-utils.ts b/frontend/src/utils/number-utils.ts index df32f4d8..5f8108fc 100644 --- a/frontend/src/utils/number-utils.ts +++ b/frontend/src/utils/number-utils.ts @@ -10,5 +10,8 @@ * @returns {string} - The rounded number as a string. */ export const roundNumber = (num: number, round: number = 2): number => { + if (isNaN(num)) { + return 0; + } return Number(num.toFixed(round)); }; From 0af9de6b1ebd8460a634402b709058a582791d43 Mon Sep 17 00:00:00 2001 From: jeafreezy Date: Thu, 13 Feb 2025 10:22:37 +0100 Subject: [PATCH 02/12] fix: better handling of number rounding --- frontend/src/features/models/components/accuracy-display.tsx | 4 +++- frontend/src/utils/number-utils.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/features/models/components/accuracy-display.tsx b/frontend/src/features/models/components/accuracy-display.tsx index 172f4842..b3eea341 100644 --- a/frontend/src/features/models/components/accuracy-display.tsx +++ b/frontend/src/features/models/components/accuracy-display.tsx @@ -1,3 +1,5 @@ +import { roundNumber } from '@/utils'; + const AccuracyDisplay = ({ accuracy }: { accuracy: number }) => { const colors = [ "bg-[#F33A0C]", @@ -18,7 +20,7 @@ const AccuracyDisplay = ({ accuracy }: { accuracy: number }) => { return (
- {accuracy.toFixed(2)} + {roundNumber(accuracy)}
diff --git a/frontend/src/utils/number-utils.ts b/frontend/src/utils/number-utils.ts index 5f8108fc..26e7e5ed 100644 --- a/frontend/src/utils/number-utils.ts +++ b/frontend/src/utils/number-utils.ts @@ -10,7 +10,7 @@ * @returns {string} - The rounded number as a string. */ export const roundNumber = (num: number, round: number = 2): number => { - if (isNaN(num)) { + if (typeof num !== "number" || isNaN(num)) { return 0; } return Number(num.toFixed(round)); From 9ea7d133d6ad9149fdc0bf28e9d1aaf5774962da Mon Sep 17 00:00:00 2001 From: jeafreezy Date: Fri, 14 Feb 2025 10:53:04 +0100 Subject: [PATCH 03/12] fix: fixed improper handling of environment variables + tests --- frontend/.env.sample | 63 ++-- frontend/package.json | 7 +- frontend/pnpm-lock.yaml | 301 +++++++++++++++++ frontend/src/app/providers/auth-provider.tsx | 8 +- .../src/app/providers/models-provider.tsx | 9 +- frontend/src/app/routes/start-mapping.tsx | 7 +- frontend/src/components/landing/kpi/kpi.tsx | 3 +- .../components/layout/navbar/user-profile.tsx | 24 +- .../components/map/controls/layer-control.tsx | 30 +- .../src/components/map/layers/basemaps.tsx | 6 +- .../components/map/layers/open-aerial-map.tsx | 6 +- .../components/map/layers/tile-boundaries.tsx | 2 +- .../components/map/setups/setup-maplibre.ts | 8 +- .../components/map/setups/setup-terra-draw.ts | 9 +- .../src/components/shared/hot-tracking.tsx | 15 +- frontend/src/config/__tests__/config.test.ts | 61 ++++ frontend/src/config/env.ts | 238 ++----------- frontend/src/config/index.ts | 319 ++++++++++++++++++ frontend/src/constants/config.ts | 269 --------------- frontend/src/constants/index.ts | 1 - .../constants/ui-contents/models-content.ts | 3 +- .../components/dialogs/file-upload-dialog.tsx | 25 +- .../training-area/training-area-item.tsx | 4 +- .../training-area/training-area-list.tsx | 20 +- .../training-area/training-area-map.tsx | 55 +-- .../hooks/use-training-areas.ts | 2 +- .../components/maps/training-area-map.tsx | 28 +- .../components/model-details-properties.tsx | 38 +-- .../start-mapping/components/header.tsx | 40 +-- .../components/logo-with-dropdown.tsx | 2 +- .../components/map/legend-control.tsx | 15 +- .../start-mapping/components/map/map.tsx | 6 +- .../components/mobile-drawer.tsx | 28 +- .../components/model-details-popup.tsx | 2 +- .../components/model-settings.tsx | 6 +- frontend/src/hooks/use-layer-order.ts | 6 +- frontend/src/hooks/use-login.ts | 3 +- frontend/src/services/api-client.ts | 9 +- frontend/src/services/api-routes.ts | 6 +- frontend/src/utils/geo/geo-utils.ts | 18 +- frontend/src/utils/geo/geometry-utils.ts | 31 +- frontend/src/utils/string-utils.ts | 2 +- 42 files changed, 993 insertions(+), 742 deletions(-) create mode 100644 frontend/src/config/__tests__/config.test.ts create mode 100644 frontend/src/config/index.ts delete mode 100644 frontend/src/constants/config.ts diff --git a/frontend/.env.sample b/frontend/.env.sample index b2d52f6e..978a3de8 100644 --- a/frontend/.env.sample +++ b/frontend/.env.sample @@ -1,8 +1,8 @@ # The backend api endpoint url. -# Data type: String (e.g., http://localhost:8000/api/v1/). -# Default value: http://localhost:8000/api/v1/. +# Data type: String (e.g., "http://localhost:8000/api/v1/"). +# Default value: "http://localhost:8000/api/v1/". # Note: Ensure CORs is enabled in the backend and access is given to your port. -VITE_BASE_API_URL = 'http://localhost:8000/api/v1/' +VITE_BASE_API_URL = "http://localhost:8000/api/v1/" # The matomo application ID. # Data type: Positive Integer (e.g., 0). @@ -10,9 +10,9 @@ VITE_BASE_API_URL = 'http://localhost:8000/api/v1/' VITE_MATOMO_ID = 0 # The matomo application domain. -# Data type: String (e.g., subdomain.hotosm.org). -# Default value: subdomain.hotosm.org. -VITE_MATOMO_APP_DOMAIN = "subdomain.hotosm.org" +# Data type: String (e.g., "fair.hotosm.org"). +# Default value: "fair.hotosm.org". +VITE_MATOMO_APP_DOMAIN = "fair.hotosm.org" # The cache duration for polling the backend for updated statistics, in seconds. # Data type: Positive Integer (e.g., 900). @@ -37,14 +37,15 @@ VITE_MAX_TRAINING_AREA_UPLOAD_FILE_SIZE = 5242880 # The current version of the application. # This is used in the OSM redirect callback when a training area is opened in OSM. -# Data type: String (e.g., v1.1). +# Data type: String (e.g., "v1.1"). # Default value: "v0.1". VITE_FAIR_VERSION = "v0.1" # Comma separated hashtags to add to the OSM ID Editor redirection. -# Data type: String (e.g., '#HOT-fAIr, #AI-Assited-Mapping'). -# Default value: `FAIR_VERSION`. -VITE_OSM_HASHTAGS = +# Data type: String (e.g., "#HOT-fAIr, #AI-Assited-Mapping"). +# Default value: "#HOT-fAIr". +# Note: please add the quotes. +VITE_OSM_HASHTAGS = "#HOT-fAIr" # The maximum zoom level for the map. # Data type: Positive Integer (e.g., 22). @@ -66,14 +67,14 @@ VITE_MIN_ZOOM_LEVEL_FOR_TRAINING_AREA_LABELS = 18 # The fill color for the training area AOI rectangles. # Data type: String (e.g., "#247DCACC"). -# Note: Colors must be hex codes or valid colors. e.g 'red', 'green', '#fff'. -# Default value: #247DCACC. +# Note: Colors must be hex codes or valid colors. e.g red, green, #fff. +# Default value: "#247DCACC". VITE_TRAINING_AREAS_AOI_FILL_COLOR = "#247DCACC" # The outline color for the training area AOI rectangles. # Data type: String (e.g., "#247DCACC"). -# Note: Colors must be hex codes or valid colors. e.g 'red', 'green', '#fff'. -# Default value: #247DCACC. +# Note: Colors must be hex codes or valid colors. e.g red, green, #fff. +# Default value: "#247DCACC". VITE_TRAINING_AREAS_AOI_OUTLINE_COLOR = "#247DCACC" # The outline width for the training area AOI rectangles. @@ -100,20 +101,20 @@ VITE_TRAINING_AREAS_AOI_LABELS_OUTLINE_WIDTH = 2 # The fill color for the training area AOI labels. # Data type: String (e.g., "#247DCACC"). -# Note: Colors must be hex codes or valid colors. e.g 'red', 'green', '#fff'. -# Default value: #D73434. +# Note: Colors must be hex codes or valid colors. e.g red, green, #fff. +# Default value: "#D73434". VITE_TRAINING_AREAS_AOI_LABELS_FILL_COLOR = "#D73434" # The outline color for the training area AOI labels. # Data type: String (e.g., "#247DCACC"). -# Note: Colors must be hex codes or valid colors. e.g 'red', 'green', '#fff'. -# Default value: #D73434. +# Note: Colors must be hex codes or valid colors. e.g red, green, #fff. +# Default value: "#D73434". VITE_TRAINING_AREAS_AOI_LABELS_OUTLINE_COLOR = "#D73434" # The remote url to JOSM. # Data type: String (e.g., "http://127.0.0.1:8111/"). -# Default value: http://127.0.0.1:8111/. +# Default value: "http://127.0.0.1:8111/". VITE_JOSM_REMOTE_URL = "http://127.0.0.1:8111/" @@ -148,22 +149,22 @@ VITE_MAX_ACCEPTABLE_POLYGON_IN_TRAINING_AREA_GEOJSON_FILE = 10 # The predictor API URL. -# Data type: String (e.g., https://predictor-dev.fair.hotosm.org/predict/). -# Default value: https://predictor-dev.fair.hotosm.org/predict/. -VITE_FAIR_PREDICTOR_API_URL = https://predictor-dev.fair.hotosm.org/predict/. +# Data type: String (e.g., "https://predictor-dev.fair.hotosm.org/predict/"). +# Default value: "https://predictor-dev.fair.hotosm.org/predict/". +VITE_FAIR_PREDICTOR_API_URL = "https://predictor-dev.fair.hotosm.org/predict/". # The OSM Database status API. -# Data type: String (e.g., https://api-prod.raw-data.hotosm.org/v1/status/). -# Default value: https://api-prod.raw-data.hotosm.org/v1/status/. -VITE_OSM_DATABASE_STATUS_API_URL = https://api-prod.raw-data.hotosm.org/v1/status/ +# Data type: String (e.g., "https://api-prod.raw-data.hotosm.org/v1/status/"). +# Default value: "https://api-prod.raw-data.hotosm.org/v1/status/". +VITE_OSM_DATABASE_STATUS_API_URL = "https://api-prod.raw-data.hotosm.org/v1/status/" # The Base URL for OAM Titiler. -# Data type: String (e.g.,https://titiler.hotosm.org). -# Default value: https://titiler.hotosm.org. -VITE_OAM_TITILER_ENDPOINT = https://titiler.hotosm.org/ +# Data type: String (e.g.,"https://titiler.hotosm.org"). +# Default value: "https://titiler.hotosm.org". +VITE_OAM_TITILER_ENDPOINT = "https://titiler.hotosm.org/" # The new S3 bucket for OAM aerial imageries. -# Data type: String (e.g.,https://oin-hotosm-temp.s3.us-east-1.amazonaws.com/). -# Default value: https://oin-hotosm-temp.s3.us-east-1.amazonaws.com/. -VITE_OAM_S3_BUCKET_URL = https://oin-hotosm-temp.s3.us-east-1.amazonaws.com/ \ No newline at end of file +# Data type: String (e.g.,"https://oin-hotosm-temp.s3.us-east-1.amazonaws.com/"). +# Default value: "https://oin-hotosm-temp.s3.us-east-1.amazonaws.com/". +VITE_OAM_S3_BUCKET_URL = "https://oin-hotosm-temp.s3.us-east-1.amazonaws.com/" \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 4353096e..2c3ad6aa 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,7 +9,9 @@ "lint": "eslint .", "preview": "vite preview", "format": "prettier --write 'src/**/*.{js,jsx,ts,tsx,json,css,scss,md}'", - "format:check": "prettier --check 'src/**/*.{js,jsx,ts,tsx,json,css,scss,md}'" + "format:check": "prettier --check 'src/**/*.{js,jsx,ts,tsx,json,css,scss,md}'", + "test": "vitest", + "coverage": "vitest run --coverage" }, "dependencies": { "@shoelace-style/shoelace": "^2.16.0", @@ -68,7 +70,8 @@ "typescript": "^5.5.3", "typescript-eslint": "^8.0.1", "vite": "^5.4.1", - "vite-tsconfig-paths": "^5.0.1" + "vite-tsconfig-paths": "^5.0.1", + "vitest": "^3.0.5" }, "pnpm": { "overrides": { diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index a5efbb1b..2824ab11 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -178,6 +178,9 @@ importers: vite-tsconfig-paths: specifier: ^5.0.1 version: 5.0.1(typescript@5.6.2)(vite@5.4.8) + vitest: + specifier: ^3.0.5 + version: 3.0.5(@types/debug@4.1.12) packages: @@ -1088,6 +1091,35 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 + '@vitest/expect@3.0.5': + resolution: {integrity: sha512-nNIOqupgZ4v5jWuQx2DSlHLEs7Q4Oh/7AYwNyE+k0UQzG7tSmjPXShUikn1mpNGzYEN2jJbTvLejwShMitovBA==} + + '@vitest/mocker@3.0.5': + resolution: {integrity: sha512-CLPNBFBIE7x6aEGbIjaQAX03ZZlBMaWwAjBdMkIf/cAn6xzLTiM3zYqO/WAbieEjsAZir6tO71mzeHZoodThvw==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.0.5': + resolution: {integrity: sha512-CjUtdmpOcm4RVtB+up8r2vVDLR16Mgm/bYdkGFe3Yj/scRfCpbSi2W/BDSDcFK7ohw8UXvjMbOp9H4fByd/cOA==} + + '@vitest/runner@3.0.5': + resolution: {integrity: sha512-BAiZFityFexZQi2yN4OX3OkJC6scwRo8EhRB0Z5HIGGgd2q+Nq29LgHU/+ovCtd0fOfXj5ZI6pwdlUmC5bpi8A==} + + '@vitest/snapshot@3.0.5': + resolution: {integrity: sha512-GJPZYcd7v8QNUJ7vRvLDmRwl+a1fGg4T/54lZXe+UOGy47F9yUfE18hRCtXL5aHN/AONu29NGzIXSVFh9K0feA==} + + '@vitest/spy@3.0.5': + resolution: {integrity: sha512-5fOzHj0WbUNqPK6blI/8VzZdkBlQLnT25knX0r4dbZI9qoZDf3qAdjoMmDcLG5A83W6oUUFJgUd0EYBc2P5xqg==} + + '@vitest/utils@3.0.5': + resolution: {integrity: sha512-N9AX0NUoUtVwKwy21JtwzaqR5L5R5A99GAbrHfCCXK1lp593i/3AZAXhSP43wRQuxYsflrdzEfXZFo1reR1Nkg==} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1177,6 +1209,10 @@ packages: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + assign-symbols@1.0.0: resolution: {integrity: sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==} engines: {node: '>=0.10.0'} @@ -1244,6 +1280,10 @@ packages: bytewise@1.1.0: resolution: {integrity: sha512-rHuuseJ9iQ0na6UDhnrRVDh8YnWVlU6xM3VH6q/+yHDeUH2zIhUzP+2/h3LIrhLDBtTqzWpE3p3tP/boefskKQ==} + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + call-bind-apply-helpers@1.0.1: resolution: {integrity: sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==} engines: {node: '>= 0.4'} @@ -1270,6 +1310,10 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@5.1.2: + resolution: {integrity: sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==} + engines: {node: '>=12'} + chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -1290,6 +1334,10 @@ packages: character-reference-invalid@2.0.1: resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} + chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -1373,9 +1421,22 @@ packages: supports-color: optional: true + debug@4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + decode-named-character-reference@1.0.2: resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -1446,6 +1507,9 @@ packages: resolution: {integrity: sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==} engines: {node: '>= 0.4'} + es-module-lexer@1.6.0: + resolution: {integrity: sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==} + es-object-atoms@1.0.0: resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} engines: {node: '>= 0.4'} @@ -1581,10 +1645,17 @@ packages: estree-util-is-identifier-name@3.0.0: resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + expect-type@1.1.0: + resolution: {integrity: sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==} + engines: {node: '>=12.0.0'} + extend-shallow@2.0.1: resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} engines: {node: '>=0.10.0'} @@ -2151,12 +2222,18 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + loupe@3.1.3: + resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + maplibre-gl@4.7.1: resolution: {integrity: sha512-lgL7XpIwsgICiL82ITplfS7IGwrB1OJIw/pCvprDp2dhmSSEBgmPzYRvwYYYvJGJD7fxUv1Tvpih4nZ6VrLuaA==} engines: {node: '>=16.14.0', npm: '>=8.1.0'} @@ -2428,6 +2505,13 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.0: + resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} + engines: {node: '>= 14.16'} + pbf@3.3.0: resolution: {integrity: sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==} hasBin: true @@ -2776,6 +2860,9 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -2806,6 +2893,12 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.8.0: + resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -2916,12 +3009,30 @@ packages: tiny-warning@1.0.3: resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinypool@1.0.2: + resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==} + engines: {node: ^18.0.0 || >=20.0.0} + tinyqueue@2.0.3: resolution: {integrity: sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==} tinyqueue@3.0.0: resolution: {integrity: sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==} + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + to-fast-properties@2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} @@ -3068,6 +3179,11 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite-node@3.0.5: + resolution: {integrity: sha512-02JEJl7SbtwSDJdYS537nU6l+ktdvcREfLksk/NDAqtdKWGqHl+joXzEubHROmS3E6pip+Xgu2tFezMu75jH7A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + vite-tsconfig-paths@5.0.1: resolution: {integrity: sha512-yqwv+LstU7NwPeNqajZzLEBVpUFU6Dugtb2P84FXuvaoYA+/70l9MHE+GYfYAycVyPSDYZ7mjOFuYBRqlEpTig==} peerDependencies: @@ -3107,6 +3223,34 @@ packages: terser: optional: true + vitest@3.0.5: + resolution: {integrity: sha512-4dof+HvqONw9bvsYxtkfUp2uHsTN9bV2CZIi1pWgoFpL1Lld8LA1ka9q/ONSsoScAKG7NVGf2stJTI7XRkXb2Q==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.0.5 + '@vitest/ui': 3.0.5 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vt-pbf@3.1.3: resolution: {integrity: sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==} @@ -3136,6 +3280,11 @@ packages: engines: {node: ^16.13.0 || >=18.0.0} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -4103,6 +4252,46 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitest/expect@3.0.5': + dependencies: + '@vitest/spy': 3.0.5 + '@vitest/utils': 3.0.5 + chai: 5.1.2 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.0.5(vite@5.4.8)': + dependencies: + '@vitest/spy': 3.0.5 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + vite: 5.4.8 + + '@vitest/pretty-format@3.0.5': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.0.5': + dependencies: + '@vitest/utils': 3.0.5 + pathe: 2.0.3 + + '@vitest/snapshot@3.0.5': + dependencies: + '@vitest/pretty-format': 3.0.5 + magic-string: 0.30.17 + pathe: 2.0.3 + + '@vitest/spy@3.0.5': + dependencies: + tinyspy: 3.0.2 + + '@vitest/utils@3.0.5': + dependencies: + '@vitest/pretty-format': 3.0.5 + loupe: 3.1.3 + tinyrainbow: 2.0.0 + acorn-jsx@5.3.2(acorn@8.12.1): dependencies: acorn: 8.12.1 @@ -4208,6 +4397,8 @@ snapshots: get-intrinsic: 1.2.6 is-array-buffer: 3.0.5 + assertion-error@2.0.1: {} + assign-symbols@1.0.0: {} ast-types-flow@0.0.8: {} @@ -4277,6 +4468,8 @@ snapshots: bytewise-core: 1.2.3 typewise: 1.0.3 + cac@6.7.14: {} + call-bind-apply-helpers@1.0.1: dependencies: es-errors: 1.3.0 @@ -4302,6 +4495,14 @@ snapshots: ccount@2.0.1: {} + chai@5.1.2: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.1.3 + pathval: 2.0.0 + chalk@2.4.2: dependencies: ansi-styles: 3.2.1 @@ -4321,6 +4522,8 @@ snapshots: character-reference-invalid@2.0.1: {} + check-error@2.1.1: {} + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -4406,10 +4609,16 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.4.0: + dependencies: + ms: 2.1.3 + decode-named-character-reference@1.0.2: dependencies: character-entities: 2.0.2 + deep-eql@5.0.2: {} + deep-is@0.1.4: {} define-data-property@1.1.4: @@ -4533,6 +4742,8 @@ snapshots: iterator.prototype: 1.1.4 safe-array-concat: 1.1.3 + es-module-lexer@1.6.0: {} + es-object-atoms@1.0.0: dependencies: es-errors: 1.3.0 @@ -4730,8 +4941,14 @@ snapshots: estree-util-is-identifier-name@3.0.0: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.6 + esutils@2.0.3: {} + expect-type@1.1.0: {} + extend-shallow@2.0.1: dependencies: is-extendable: 0.1.1 @@ -5323,12 +5540,18 @@ snapshots: dependencies: js-tokens: 4.0.0 + loupe@3.1.3: {} + lru-cache@10.4.3: {} lru-cache@5.1.1: dependencies: yallist: 3.1.1 + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + maplibre-gl@4.7.1: dependencies: '@mapbox/geojson-rewind': 0.5.2 @@ -5839,6 +6062,10 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 + pathe@2.0.3: {} + + pathval@2.0.0: {} + pbf@3.3.0: dependencies: ieee754: 1.2.1 @@ -6252,6 +6479,8 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + signal-exit@4.1.0: {} sort-asc@0.2.0: {} @@ -6277,6 +6506,10 @@ snapshots: sprintf-js@1.0.3: {} + stackback@0.0.2: {} + + std-env@3.8.0: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -6444,10 +6677,20 @@ snapshots: tiny-warning@1.0.3: {} + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinypool@1.0.2: {} + tinyqueue@2.0.3: {} tinyqueue@3.0.0: {} + tinyrainbow@2.0.0: {} + + tinyspy@3.0.2: {} + to-fast-properties@2.0.0: {} to-regex-range@5.0.1: @@ -6619,6 +6862,24 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 + vite-node@3.0.5: + dependencies: + cac: 6.7.14 + debug: 4.4.0 + es-module-lexer: 1.6.0 + pathe: 2.0.3 + vite: 5.4.8 + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vite-tsconfig-paths@5.0.1(typescript@5.6.2)(vite@5.4.8): dependencies: debug: 4.3.7 @@ -6638,6 +6899,41 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + vitest@3.0.5(@types/debug@4.1.12): + dependencies: + '@vitest/expect': 3.0.5 + '@vitest/mocker': 3.0.5(vite@5.4.8) + '@vitest/pretty-format': 3.0.5 + '@vitest/runner': 3.0.5 + '@vitest/snapshot': 3.0.5 + '@vitest/spy': 3.0.5 + '@vitest/utils': 3.0.5 + chai: 5.1.2 + debug: 4.4.0 + expect-type: 1.1.0 + magic-string: 0.30.17 + pathe: 2.0.3 + std-env: 3.8.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.0.2 + tinyrainbow: 2.0.0 + vite: 5.4.8 + vite-node: 3.0.5 + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/debug': 4.1.12 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vt-pbf@3.1.3: dependencies: '@mapbox/point-geometry': 0.1.0 @@ -6692,6 +6988,11 @@ snapshots: dependencies: isexe: 3.1.1 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + word-wrap@1.2.5: {} wrap-ansi@7.0.0: diff --git a/frontend/src/app/providers/auth-provider.tsx b/frontend/src/app/providers/auth-provider.tsx index f54e97d1..83983829 100644 --- a/frontend/src/app/providers/auth-provider.tsx +++ b/frontend/src/app/providers/auth-provider.tsx @@ -3,19 +3,19 @@ import React, { useContext, useEffect, useState -} from 'react'; + } from 'react'; import { apiClient } from '@/services/api-client'; import { authService } from '@/services'; +import { HOT_FAIR_LOCAL_STORAGE_ACCESS_TOKEN_KEY, HOT_FAIR_LOGIN_SUCCESSFUL_SESSION_KEY, HOT_FAIR_SESSION_REDIRECT_KEY } from '@/config'; import { showErrorToast, showSuccessToast } from '@/utils'; import { TUser } from '@/types/api'; import { useLocalStorage, useSessionStorage } from '@/hooks/use-storage'; import { TOAST_NOTIFICATIONS, - HOT_FAIR_LOCAL_STORAGE_ACCESS_TOKEN_KEY, - HOT_FAIR_LOGIN_SUCCESSFUL_SESSION_KEY, - HOT_FAIR_SESSION_REDIRECT_KEY, } from "@/constants"; + + type TAuthContext = { token: string; user: TUser; diff --git a/frontend/src/app/providers/models-provider.tsx b/frontend/src/app/providers/models-provider.tsx index 7d47b7d6..f3affbc2 100644 --- a/frontend/src/app/providers/models-provider.tsx +++ b/frontend/src/app/providers/models-provider.tsx @@ -1,18 +1,19 @@ import { APPLICATION_ROUTES, - HOT_FAIR_MODEL_CREATION_SESSION_STORAGE_KEY, MODELS_BASE, - MODELS_ROUTES -} from '@/constants'; + MODELS_ROUTES, + TOAST_NOTIFICATIONS + } from '@/constants'; import { BASE_MODELS, TrainingDatasetOption, TrainingType } from '@/enums'; +import { HOT_FAIR_MODEL_CREATION_SESSION_STORAGE_KEY } from '@/config'; import { LngLatBoundsLike } from 'maplibre-gl'; -import { TOAST_NOTIFICATIONS } from '@/constants'; import { useCreateTrainingDataset } from '@/features/model-creation/hooks/use-training-datasets'; import { useGetTrainingDataset } from '@/features/models/hooks/use-dataset'; import { useLocation, useNavigate, useParams } from 'react-router-dom'; import { useModelDetails } from '@/features/models/hooks/use-models'; import { UseMutationResult } from '@tanstack/react-query'; import { useSessionStorage } from '@/hooks/use-storage'; + import { TTrainingAreaFeature, TTrainingDataset, diff --git a/frontend/src/app/routes/start-mapping.tsx b/frontend/src/app/routes/start-mapping.tsx index 8d84ea45..df642dee 100644 --- a/frontend/src/app/routes/start-mapping.tsx +++ b/frontend/src/app/routes/start-mapping.tsx @@ -1,5 +1,5 @@ import useScreenSize from '@/hooks/use-screen-size'; -import { APPLICATION_ROUTES } from '@/constants'; +import { APPLICATION_ROUTES, START_MAPPING_PAGE_CONTENT, TOAST_NOTIFICATIONS } from '@/constants'; import { BASE_MODELS } from '@/enums'; import { FitToBounds, LayerControl, ZoomLevel } from '@/components/map'; import { Head } from '@/components/seo'; @@ -34,8 +34,7 @@ import { showSuccessToast, } from "@/utils"; import { - START_MAPPING_PAGE_CONTENT, - TOAST_NOTIFICATIONS, + PREDICTION_API_FILE_EXTENSIONS, REJECTED_MODEL_PREDICTIONS_FILL_LAYER_ID, REJECTED_MODEL_PREDICTIONS_OUTLINE_LAYER_ID, @@ -44,7 +43,7 @@ import { ACCEPTED_MODEL_PREDICTIONS_OUTLINE_LAYER_ID, ALL_MODEL_PREDICTIONS_FILL_LAYER_ID, ALL_MODEL_PREDICTIONS_OUTLINE_LAYER_ID, -} from "@/constants"; +} from "@/config"; export type TDownloadOptions = { name: string; diff --git a/frontend/src/components/landing/kpi/kpi.tsx b/frontend/src/components/landing/kpi/kpi.tsx index 9cc263e0..82320e98 100644 --- a/frontend/src/components/landing/kpi/kpi.tsx +++ b/frontend/src/components/landing/kpi/kpi.tsx @@ -1,6 +1,7 @@ import styles from './kpi.module.css'; import { API_ENDPOINTS, apiClient } from '@/services'; -import { KPI_STATS_CACHE_TIME_MS, SHARED_CONTENT } from '@/constants'; +import { KPI_STATS_CACHE_TIME_MS } from '@/config'; +import { SHARED_CONTENT } from '@/constants'; import { useQuery } from '@tanstack/react-query'; type TKPIS = { figure?: number; diff --git a/frontend/src/components/layout/navbar/user-profile.tsx b/frontend/src/components/layout/navbar/user-profile.tsx index 082f8a8f..6aa56039 100644 --- a/frontend/src/components/layout/navbar/user-profile.tsx +++ b/frontend/src/components/layout/navbar/user-profile.tsx @@ -1,19 +1,21 @@ -import SlAvatar from "@shoelace-style/shoelace/dist/react/avatar/index.js"; -import styles from "@/components/layout/navbar/navbar.module.css"; -import useScreenSize from "@/hooks/use-screen-size"; -import { DropDown } from "@/components/ui/dropdown"; -import { DropdownPlacement } from "@/enums"; -import { TCSSWithVars } from "@/types"; -import { truncateString } from "@/utils"; -import { useAuth } from "@/app/providers/auth-provider"; -import { useDropdownMenu } from "@/hooks/use-dropdown-menu"; -import { useNavigate } from "react-router-dom"; +import SlAvatar from '@shoelace-style/shoelace/dist/react/avatar/index.js'; +import styles from '@/components/layout/navbar/navbar.module.css'; +import useScreenSize from '@/hooks/use-screen-size'; +import { DropDown } from '@/components/ui/dropdown'; +import { DropdownPlacement } from '@/enums'; +import { ELEMENT_DISTANCE_FROM_NAVBAR } from '@/config'; +import { TCSSWithVars } from '@/types'; +import { truncateString } from '@/utils'; +import { useAuth } from '@/app/providers/auth-provider'; +import { useDropdownMenu } from '@/hooks/use-dropdown-menu'; +import { useNavigate } from 'react-router-dom'; import { APPLICATION_ROUTES, - ELEMENT_DISTANCE_FROM_NAVBAR, + SHARED_CONTENT, } from "@/constants"; + export const UserProfile = ({ hideFullName, smallerSize, diff --git a/frontend/src/components/map/controls/layer-control.tsx b/frontend/src/components/map/controls/layer-control.tsx index c2dbba5e..da744fad 100644 --- a/frontend/src/components/map/controls/layer-control.tsx +++ b/frontend/src/components/map/controls/layer-control.tsx @@ -1,16 +1,16 @@ -import { BASEMAPS, ToolTipPlacement } from "@/enums"; -import { CheckboxGroup } from "@/components/ui/form"; -import { DropDown } from "@/components/ui/dropdown"; -import { LayerStackIcon } from "@/components/ui/icons"; -import { Map } from "maplibre-gl"; -import { ToolTip } from "@/components/ui/tooltip"; -import { useDropdownMenu } from "@/hooks/use-dropdown-menu"; -import { useEffect, useMemo, useState } from "react"; +import { BASEMAPS, ToolTipPlacement } from '@/enums'; +import { CheckboxGroup } from '@/components/ui/form'; +import { DropDown } from '@/components/ui/dropdown'; +import { LayerStackIcon } from '@/components/ui/icons'; +import { Map } from 'maplibre-gl'; +import { ToolTip } from '@/components/ui/tooltip'; +import { useDropdownMenu } from '@/hooks/use-dropdown-menu'; +import { useEffect, useMemo, useState } from 'react'; import { GOOGLE_SATELLITE_BASEMAP_LAYER_ID, OSM_BASEMAP_LAYER_ID, TMS_LAYER_ID, -} from "@/constants"; +} from "@/config"; type TLayers = { id?: string; subLayers: string[]; value: string }[]; type TBasemaps = { id?: string; subLayer: string; value: string }[]; @@ -38,12 +38,12 @@ export const LayerControl = ({ ]; const baseLayers: TBasemaps = basemaps ? [ - { value: BASEMAPS.OSM, subLayer: OSM_BASEMAP_LAYER_ID }, - { - value: BASEMAPS.GOOGLE_SATELLITE, - subLayer: GOOGLE_SATELLITE_BASEMAP_LAYER_ID, - }, - ] + { value: BASEMAPS.OSM, subLayer: OSM_BASEMAP_LAYER_ID }, + { + value: BASEMAPS.GOOGLE_SATELLITE, + subLayer: GOOGLE_SATELLITE_BASEMAP_LAYER_ID, + }, + ] : []; return { layers_, baseLayers }; }, [layers, openAerialMap, basemaps]); diff --git a/frontend/src/components/map/layers/basemaps.tsx b/frontend/src/components/map/layers/basemaps.tsx index 87c4f007..147bcc1a 100644 --- a/frontend/src/components/map/layers/basemaps.tsx +++ b/frontend/src/components/map/layers/basemaps.tsx @@ -1,9 +1,9 @@ -import { Map } from "maplibre-gl"; -import { useMapLayers } from "@/hooks/use-map-layer"; +import { Map } from 'maplibre-gl'; +import { useMapLayers } from '@/hooks/use-map-layer'; import { GOOGLE_SATELLITE_BASEMAP_LAYER_ID, GOOGLE_SATELLITE_BASEMAP_SOURCE_ID, -} from "@/constants"; +} from "@/config"; export const Basemaps = ({ map }: { map: Map | null }) => { useMapLayers( diff --git a/frontend/src/components/map/layers/open-aerial-map.tsx b/frontend/src/components/map/layers/open-aerial-map.tsx index 2a8ef186..f7816e46 100644 --- a/frontend/src/components/map/layers/open-aerial-map.tsx +++ b/frontend/src/components/map/layers/open-aerial-map.tsx @@ -1,6 +1,6 @@ -import { Map } from "maplibre-gl"; -import { TMS_LAYER_ID, TMS_SOURCE_ID } from "@/constants"; -import { useMapLayers } from "@/hooks/use-map-layer"; +import { Map } from 'maplibre-gl'; +import { TMS_LAYER_ID, TMS_SOURCE_ID } from '@/config'; +import { useMapLayers } from '@/hooks/use-map-layer'; export const OpenAerialMap = ({ tileJSONURL, diff --git a/frontend/src/components/map/layers/tile-boundaries.tsx b/frontend/src/components/map/layers/tile-boundaries.tsx index 742b2397..c3d08e3b 100644 --- a/frontend/src/components/map/layers/tile-boundaries.tsx +++ b/frontend/src/components/map/layers/tile-boundaries.tsx @@ -1,7 +1,7 @@ import { GeoJSONSource, Map } from 'maplibre-gl'; import { GeoJSONType } from '@/types'; import { getTileBoundariesGeoJSON } from '@/utils'; -import { TILE_BOUNDARY_LAYER_ID, TILE_BOUNDARY_SOURCE_ID } from '@/constants'; +import { TILE_BOUNDARY_LAYER_ID, TILE_BOUNDARY_SOURCE_ID } from '@/config'; import { useCallback, useEffect } from 'react'; import { useMapLayers } from '@/hooks/use-map-layer'; diff --git a/frontend/src/components/map/setups/setup-maplibre.ts b/frontend/src/components/map/setups/setup-maplibre.ts index 53e5745f..5698845d 100644 --- a/frontend/src/components/map/setups/setup-maplibre.ts +++ b/frontend/src/components/map/setups/setup-maplibre.ts @@ -1,7 +1,7 @@ -import maplibregl, { Map } from "maplibre-gl"; -import { BASEMAPS } from "@/enums"; -import { MAP_STYLES, MAX_ZOOM_LEVEL } from "@/constants"; -import { Protocol } from "pmtiles"; +import maplibregl, { Map } from 'maplibre-gl'; +import { BASEMAPS } from '@/enums'; +import { MAP_STYLES, MAX_ZOOM_LEVEL } from '@/config'; +import { Protocol } from 'pmtiles'; export const setupMaplibreMap = ( containerRef: React.RefObject, diff --git a/frontend/src/components/map/setups/setup-terra-draw.ts b/frontend/src/components/map/setups/setup-terra-draw.ts index fbc4e39a..0a244816 100644 --- a/frontend/src/components/map/setups/setup-terra-draw.ts +++ b/frontend/src/components/map/setups/setup-terra-draw.ts @@ -1,4 +1,5 @@ -import maplibregl from "maplibre-gl"; +import maplibregl from 'maplibre-gl'; +import { HexColorStyling } from 'node_modules/terra-draw/dist/common'; import { TerraDraw, TerraDrawMapLibreGLAdapter, @@ -10,7 +11,7 @@ import { TRAINING_AREAS_AOI_FILL_OPACITY, TRAINING_AREAS_AOI_OUTLINE_COLOR, TRAINING_AREAS_AOI_OUTLINE_WIDTH, -} from "@/constants"; +} from "@/config"; export const setupTerraDraw = (map: maplibregl.Map) => { return new TerraDraw({ @@ -53,13 +54,13 @@ export const setupTerraDraw = (map: maplibregl.Map) => { }, styles: { // Fill colour (a string containing a 6 digit Hex color) - fillColor: TRAINING_AREAS_AOI_FILL_COLOR, + fillColor: TRAINING_AREAS_AOI_FILL_COLOR as HexColorStyling, // Fill opacity (0 - 1) fillOpacity: TRAINING_AREAS_AOI_FILL_OPACITY, // Outline colour (Hex color) - outlineColor: TRAINING_AREAS_AOI_OUTLINE_COLOR, + outlineColor: TRAINING_AREAS_AOI_OUTLINE_COLOR as HexColorStyling, //Outline width (Integer) outlineWidth: TRAINING_AREAS_AOI_OUTLINE_WIDTH, diff --git a/frontend/src/components/shared/hot-tracking.tsx b/frontend/src/components/shared/hot-tracking.tsx index a18bf0d7..826d645a 100644 --- a/frontend/src/components/shared/hot-tracking.tsx +++ b/frontend/src/components/shared/hot-tracking.tsx @@ -1,8 +1,9 @@ -import { APPLICATION_ROUTES } from "@/constants"; -import { ENVS } from "@/config/env"; -import { HOT_TRACKING_HTML_TAG_NAME } from "@/constants"; -import { useEffect } from "react"; -import { useLocation } from "react-router-dom"; +import { APPLICATION_ROUTES } from '@/constants'; +import { HOT_TRACKING_HTML_TAG_NAME, MATOMO_APP_DOMAIN, MATOMO_ID } from '@/config'; +import { useEffect } from 'react'; +import { useLocation } from 'react-router-dom'; + + export const HotTracking = ({ homepagePath = APPLICATION_ROUTES.HOMEPAGE }) => { const { pathname } = useLocation(); @@ -15,8 +16,8 @@ export const HotTracking = ({ homepagePath = APPLICATION_ROUTES.HOMEPAGE }) => { const hotTracking = document.createElement(HOT_TRACKING_HTML_TAG_NAME); // CSS classname to customize it hotTracking.classList.add("hot-matomo"); - hotTracking.setAttribute("site-id", ENVS.MATOMO_ID); - hotTracking.setAttribute("domain", ENVS.MATOMO_APP_DOMAIN); + hotTracking.setAttribute("site-id", MATOMO_ID); + hotTracking.setAttribute("domain", MATOMO_APP_DOMAIN); hotTracking.setAttribute("force", "true"); // Append element to body diff --git a/frontend/src/config/__tests__/config.test.ts b/frontend/src/config/__tests__/config.test.ts new file mode 100644 index 00000000..37d2c2da --- /dev/null +++ b/frontend/src/config/__tests__/config.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it } from 'vitest'; +import { parseFloatEnv, parseIntEnv, parseStringEnv } from '../index'; + + +describe("parseIntEnv()", () => { + it("should return the integer value when a valid number is provided", () => { + expect(parseIntEnv("10", 5)).toBe(10); + }); + + it("should return the default value when input is undefined", () => { + expect(parseIntEnv(undefined, 5)).toBe(5); + }); + + it("should return the default value when input is NaN", () => { + expect(parseIntEnv("invalid", 5)).toBe(5); + }); + + it("should return 0 when input is '0'", () => { + expect(parseIntEnv("0", 5)).toBe(0); + }); +}); + +describe("parseFloatEnv()", () => { + it("should return the float value when a valid number is provided", () => { + expect(parseFloatEnv("10.5", 5.5)).toBe(10.5); + }); + + it("should return the default value when input is undefined", () => { + expect(parseFloatEnv(undefined, 5.5)).toBe(5.5); + }); + + it("should return the default value when input is NaN", () => { + expect(parseFloatEnv("invalid", 5.5)).toBe(5.5); + }); + + it("should return 0 when input is '0'", () => { + expect(parseFloatEnv("0", 5.5)).toBe(0); + }); +}); + +describe("parseStringEnv()", () => { + it("should return the provided value when it is a valid string", () => { + expect(parseStringEnv("example.com", "default.com")).toBe("example.com"); + }); + + it("should return the default value when input is undefined", () => { + expect(parseStringEnv(undefined, "default.com")).toBe("default.com"); + }); + + it("should return the default value when input is an empty string", () => { + expect(parseStringEnv("", "default.com")).toBe("default.com"); + }); + + it("should return the default value when input is only spaces", () => { + expect(parseStringEnv(" ", "default.com")).toBe("default.com"); + }); + + it("should trim spaces from a valid input", () => { + expect(parseStringEnv(" example.com ", "default.com")).toBe("example.com"); + }); +}); diff --git a/frontend/src/config/env.ts b/frontend/src/config/env.ts index d407d478..adeeeec8 100644 --- a/frontend/src/config/env.ts +++ b/frontend/src/config/env.ts @@ -1,265 +1,65 @@ /** - * The environment variables. Ideally these values should be set in the .env file. + * The environment variables. */ export const ENVS = { - /** - The backend api endpoint url. - Data type: String (e.g., http://localhost:8000/api/v1/). - Default value: http://localhost:8000/api/v1/. - Note: Ensure CORs is enabled in the backend and access is given to your port. - */ - BASE_API_URL: import.meta.env.VITE_BASE_API_URL, - /** - The matomo application ID. - Data type: Positive Integer (e.g., 0). - Default value: 0. - */ - MATOMO_ID: import.meta.env.VITE_MATOMO_ID, - /** - The matomo application domain. - Data type: String (e.g., subdomain.hotosm.org). - Default value: subdomain.hotosm.org. - */ - MATOMO_APP_DOMAIN: import.meta.env.VITE_MATOMO_APP_DOMAIN, - /** - The cache duration for polling the backend for updated statistics, in seconds. - Data type: Positive Integer (e.g., 900). - Default value: 900 seconds (15 minutes). - Note: If this value changes on the backend, please update it here to avoid unnecessary polling. - */ - KPI_STATS_CACHE_TIME: import.meta.env.VITE_KPI_STATS_CACHE_TIME, - /** - The maximum allowed area size for training areas, measured in square meters. - Data type: Positive Integer (e.g., 5000000). - Default value: 5000000 square meters (5 square kilometers). - */ - MAX_TRAINING_AREA_SIZE: import.meta.env.VITE_MAX_TRAINING_AREA_SIZE, - /** - The minumum allowed area size for training areas, measured in square meters. - Data type: Positive Integer (e.g., 5797). - Default value: 5797 square meters. - */ - MIN_TRAINING_AREA_SIZE: import.meta.env.VITE_MIN_TRAINING_AREA_SIZE, - /** - The maximum file size allowed for training area upload, measure in bytes. - Data type: Positive Integer (e.g., 500000). - Default value: 5242880 bytes (5 MB). - */ - - MAX_TRAINING_AREA_UPLOAD_FILE_SIZE: import.meta.env - .VITE_MAX_TRAINING_AREA_UPLOAD_FILE_SIZE, - - /** - The current version of the application. - This is used in the OSM redirect callback when a training area is opened in OSM. - Data type: String (e.g., v1.1). - Default value: "v0.1". - */ + MAX_TRAINING_AREA_UPLOAD_FILE_SIZE: import.meta.env.VITE_MAX_TRAINING_AREA_UPLOAD_FILE_SIZE, FAIR_VERSION: import.meta.env.VITE_FAIR_VERSION, - /** - Comma separated hashtags to add to the OSM ID Editor redirection. - Data type: String (e.g., 'HOT-fAIr, AI-Assited-Mapping'). - Default value: `FAIR_VERSION`. - */ - OSM_HASHTAGS: import.meta.env.VITE_OSM_HASHTAGS, - /** - The maximum zoom level for the map. - Data type: Positive Integer (e.g., 22). - Note: Value must be between 0 - 24. - Default value: 22. - */ - MAX_ZOOM_LEVEL: import.meta.env.VITE_MAX_ZOOM_LEVEL, - /** - The minimum zoom level before enabling the prediction button and other functionalities in the start mapping page. - Data type: Positive Integer (e.g., 22). - Note: Value must be between 0 - 24. - Default value: 19. - */ - - MIN_ZOOM_LEVEL_FOR_START_MAPPING_PREDICTION: import.meta.env - .VITE_MIN_ZOOM_LEVEL_FOR_START_MAPPING_PREDICTION, - - /** - The minimum zoom level before enabling the training area labels in the training area map. - Data type: Positive Integer (e.g., 22). - Note: Value must be between 0 - 24. - Default value: 18. - */ - - MIN_ZOOM_LEVEL_FOR_TRAINING_AREA_LABELS: import.meta.env - .VITE_MIN_ZOOM_LEVEL_FOR_TRAINING_AREA_LABELS, + MIN_ZOOM_LEVEL_FOR_START_MAPPING_PREDICTION: import.meta.env.VITE_MIN_ZOOM_LEVEL_FOR_START_MAPPING_PREDICTION, - /** - The fill color for the training area AOI rectangles. - Data type: String (e.g., "247DCACC"). - Note: Colors must be hex codes or valid colors. e.g 'red', 'green', 'fff'. - Default value: 247DCACC. - */ + MIN_ZOOM_LEVEL_FOR_TRAINING_AREA_LABELS: import.meta.env.VITE_MIN_ZOOM_LEVEL_FOR_TRAINING_AREA_LABELS, - TRAINING_AREAS_AOI_FILL_COLOR: import.meta.env - .VITE_TRAINING_AREAS_AOI_FILL_COLOR, + TRAINING_AREAS_AOI_FILL_COLOR: import.meta.env.VITE_TRAINING_AREAS_AOI_FILL_COLOR, - /** - The outline color for the training area AOI rectangles. - Data type: String (e.g., "247DCACC"). - Note: Colors must be hex codes or valid colors. e.g 'red', 'green', 'fff'. - Default value: 247DCACC. - */ + TRAINING_AREAS_AOI_OUTLINE_COLOR: import.meta.env.VITE_TRAINING_AREAS_AOI_OUTLINE_COLOR, - TRAINING_AREAS_AOI_OUTLINE_COLOR: import.meta.env - .VITE_TRAINING_AREAS_AOI_OUTLINE_COLOR, + TRAINING_AREAS_AOI_OUTLINE_WIDTH: import.meta.env.VITE_TRAINING_AREAS_AOI_OUTLINE_WIDTH, - /** - The outline width for the training area AOI rectangles. - Data type: Positive Integer (e.g., 3). - Default value: 4. - */ + TRAINING_AREAS_AOI_FILL_OPACITY: import.meta.env.VITE_TRAINING_AREAS_AOI_FILL_OPACITY, - TRAINING_AREAS_AOI_OUTLINE_WIDTH: import.meta.env - .VITE_TRAINING_AREAS_AOI_OUTLINE_WIDTH, + TRAINING_AREAS_AOI_LABELS_FILL_OPACITY: import.meta.env.VITE_TRAINING_AREAS_AOI_LABELS_FILL_OPACITY, - /** - The fill opacity for the training area AOI rectangles. - Data type: Float (e.g., 0.4). - Note: Value must be between 0 and 1. - Default value: 0.4. - */ + TRAINING_AREAS_AOI_LABELS_OUTLINE_WIDTH: import.meta.env.VITE_TRAINING_AREAS_AOI_LABELS_OUTLINE_WIDTH, - TRAINING_AREAS_AOI_FILL_OPACITY: import.meta.env - .VITE_TRAINING_AREAS_AOI_FILL_OPACITY, + TRAINING_AREAS_AOI_LABELS_FILL_COLOR: import.meta.env.VITE_TRAINING_AREAS_AOI_LABELS_FILL_COLOR, - /** - The fill opacity for the training area AOI labels. - Data type: Float (e.g., 0.4). - Note: Value must be between 0 and 1. - Default value: 0.3. - */ - - TRAINING_AREAS_AOI_LABELS_FILL_OPACITY: import.meta.env - .VITE_TRAINING_AREAS_AOI_LABELS_FILL_OPACITY, - - /** - The outline width for the training area AOI labels. - Data type: Positive Integer (e.g., 3). - Default value: 2. - */ - - TRAINING_AREAS_AOI_LABELS_OUTLINE_WIDTH: import.meta.env - .VITE_TRAINING_AREAS_AOI_LABELS_OUTLINE_WIDTH, - - /** - The fill color for the training area AOI labels. - Data type: String (e.g., "247DCACC"). - Note: Colors must be hex codes or valid colors. e.g 'red', 'green', 'fff'. - Default value: D73434. - */ - - TRAINING_AREAS_AOI_LABELS_FILL_COLOR: import.meta.env - .VITE_TRAINING_AREAS_AOI_LABELS_FILL_COLOR, - - /** - The outline color for the training area AOI labels. - Data type: String (e.g., "247DCACC"). - Note: Colors must be hex codes or valid colors. e.g 'red', 'green', 'fff'. - Default value: D73434. - */ - - TRAINING_AREAS_AOI_LABELS_OUTLINE_COLOR: import.meta.env - .VITE_TRAINING_AREAS_AOI_LABELS_OUTLINE_COLOR, - - /** - The remote url to JOSM. - Data type: String (e.g., "http://127.0.0.1:8111/"). - Default value: http://127.0.0.1:8111/. - */ + TRAINING_AREAS_AOI_LABELS_OUTLINE_COLOR: import.meta.env.VITE_TRAINING_AREAS_AOI_LABELS_OUTLINE_COLOR, JOSM_REMOTE_URL: import.meta.env.VITE_JOSM_REMOTE_URL, - /** - The time to poll the backend for the status of the AOI training labels fetching, in milliseconds (ms). - Data type: Positive Integer (e.g., 900). - Default value: 5000 milliseconds (5 seconds). - */ - TRAINING_AREA_LABELS_FETCH_POOLING_INTERVAL_MS: import.meta.env - .VITE_TRAINING_AREA_LABELS_FETCH_POOLING_INTERVAL_MS, - - /** - The time to poll the backend for the status of the OSM last updated time, in milliseconds (ms). - Data type: Positive Integer (e.g., 900). - Default value: 10000 milliseconds (10 seconds). - */ + TRAINING_AREA_LABELS_FETCH_POOLING_INTERVAL_MS: import.meta.env.VITE_TRAINING_AREA_LABELS_FETCH_POOLING_INTERVAL_MS, - OSM_LAST_UPDATED_POOLING_INTERVAL_MS: import.meta.env - .VITE_OSM_LAST_UPDATED_POOLING_INTERVAL_MS, + OSM_LAST_UPDATED_POOLING_INTERVAL_MS: import.meta.env.VITE_OSM_LAST_UPDATED_POOLING_INTERVAL_MS, - /** - The maximum GeoJSON file containing the training labels, a user can upload for an AOI. - Data type: Positive Integer (e.g., 1). - Default value: 1 (1 GeoJSON file). - */ - MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREA_LABELS: import.meta.env - .VITE_MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREA_LABELS, + MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREA_LABELS: import.meta.env.VITE_MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREA_LABELS, - /** - The maximum GeoJSON file(s) containing the training areas/AOI polygon geometry that a user can upload. - Data type: Positive Integer (e.g., 1). - Default value: 10 (10 GeoJSON files, assumming each file has a single AOI). - */ - MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREAS: import.meta.env - .VITE_MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREAS, + MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREAS: import.meta.env.VITE_MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREAS, - /** - The maximum GeoJSON file(s) containing the training areas/AOI polygon geometry that a user can upload. - Data type: Positive Integer (e.g., 1). - Default value: 10 (10 GeoJSON files, assumming each file has a single AOI). - */ - MAX_ACCEPTABLE_POLYGON_IN_TRAINING_AREA_GEOJSON_FILE: import.meta.env - .VITE_MAX_ACCEPTABLE_POLYGON_IN_TRAINING_AREA_GEOJSON_FILE, + MAX_ACCEPTABLE_POLYGON_IN_TRAINING_AREA_GEOJSON_FILE: import.meta.env.VITE_MAX_ACCEPTABLE_POLYGON_IN_TRAINING_AREA_GEOJSON_FILE, - /** - The predictor API URL. - Data type: String (e.g., https://predictor-dev.fair.hotosm.org/predict/). - Default value: https://predictor-dev.fair.hotosm.org/predict/. - */ FAIR_PREDICTOR_API_URL: import.meta.env.VITE_FAIR_PREDICTOR_API_URL, - /** - The OSM Database status API. - Data type: String (e.g., https://api-prod.raw-data.hotosm.org/v1/status/). - Default value: https://api-prod.raw-data.hotosm.org/v1/status/. - */ + OSM_DATABASE_STATUS_API_URL: import.meta.env.VITE_OSM_DATABASE_STATUS_API_URL, - /** - The Base URL for OAM's Titiler. - Data type: String (e.g.,https://titiler.hotosm.org/). - Default value: https://titiler.hotosm.org/. - */ OAM_TITILER_ENDPOINT: import.meta.env.VITE_OAM_TITILER_ENDPOINT, - /** - The new S3 bucket for OAM aerial imageries. - Data type: String (e.g.,https://oin-hotosm-temp.s3.us-east-1.amazonaws.com/). - Default value: https://oin-hotosm-temp.s3.us-east-1.amazonaws.com/. - */ - OAM_S3_BUCKET_URL: import.meta.env.VITE_OAM_S3_BUCKET_URL, - }; + diff --git a/frontend/src/config/index.ts b/frontend/src/config/index.ts new file mode 100644 index 00000000..5dae6282 --- /dev/null +++ b/frontend/src/config/index.ts @@ -0,0 +1,319 @@ +import { BASE_MODELS } from '@/enums'; +import { ENVS } from '@/config/env'; +import { StyleSpecification } from 'maplibre-gl'; + + +// ============================================================================================================================== +// Helper functions +// ============================================================================================================================== + + + +/** + * Helper function to safely parse environment variables as integers. + */ +export const parseIntEnv = (value: string | undefined, defaultValue: number): number => + value !== undefined && !isNaN(parseInt(value, 10)) ? parseInt(value, 10) : defaultValue; + +/** + * Helper function to safely parse environment variables as floats. + */ +export const parseFloatEnv = (value: string | undefined, defaultValue: number): number => + value !== undefined && !isNaN(parseFloat(value)) ? parseFloat(value) : defaultValue; + +/** + * Helper function to safely parse environment variables as strings. + */ +export const parseStringEnv = (value: string | undefined, defaultValue: string): string => + value && value.trim() !== "" ? value.trim() : defaultValue; + + +// ============================================================================================================================== +// API Endpoints +// ============================================================================================================================== + + + +/** + * The backend api endpoint url. + * Note: Ensure CORs is enabled in the backend and access is given to your port. +*/ +export const BASE_API_URL: string = parseStringEnv(ENVS.BASE_API_URL, "http://localhost:8000/api/v1/"); + +/** + * The Base URL for OAM's Titiler. +*/ +export const OAM_TITILER_ENDPOINT: string = parseStringEnv(ENVS.OAM_TITILER_ENDPOINT, "https://titiler.hotosm.org/"); + +/** + * The new S3 bucket for OAM aerial imageries. +*/ +export const OAM_S3_BUCKET_URL: string = parseStringEnv(ENVS.OAM_S3_BUCKET_URL, "https://oin-hotosm-temp.s3.us-east-1.amazonaws.com/"); + +/** + * The remote url to JOSM. + */ +export const JOSM_REMOTE_URL: string = parseStringEnv(ENVS.JOSM_REMOTE_URL, "http://127.0.0.1:8111/"); + +/** + * The OSM Database status API endpoint. + */ +export const OSM_DATABASE_STATUS_API_ENDPOINT: string = parseStringEnv(ENVS.OSM_DATABASE_STATUS_API_URL, "https://api-prod.raw-data.hotosm.org/v1/status/"); + +/** + * The model prediction endpoint. + */ +export const FAIR_PREDICTOR_API_ENDPOINT: string = parseStringEnv(ENVS.FAIR_PREDICTOR_API_URL, "https://predictor-dev.fair.hotosm.org/predict/"); + + + +// ============================================================================================================================== +// Local & Session Storage Keys +// ============================================================================================================================== + + + +/** + * The key used to store the access token in local storage for the application. + */ +export const HOT_FAIR_LOCAL_STORAGE_ACCESS_TOKEN_KEY: string = "___hot_fAIr_access_token"; + +/** + * The key used to store the redirect URL after login in session storage for the application. + */ +export const HOT_FAIR_SESSION_REDIRECT_KEY: string = "___hot_fAIr_redirect_after_login"; + +/** + * The key used to indicate a successful login session for the application. + */ +export const HOT_FAIR_LOGIN_SUCCESSFUL_SESSION_KEY: string = "__hot_fair_login_successful"; + +/** + * The key used to store the model form data in session storage to preserve the state incase the user + * visits ID Editor or JOSM to map a training area. + * Session storage is used to allow users to be able to open fAIr on a new tab and start on a clean slate. + */ +export const HOT_FAIR_MODEL_CREATION_SESSION_STORAGE_KEY: string = "__hot_fair_model_creation_formdata"; + +/** + * The key used to store the banner state in local storage for the application. + */ +export const HOT_FAIR_BANNER_LOCAL_STORAGE_KEY: string = "__hot_fair_banner_closed"; + +/** + * The key used to store the model predictions in the session storage for the application. + */ +export const HOT_FAIR_MODEL_PREDICTIONS_SESSION_STORAGE_KEY: string = "__hot_fair_model_predictions"; + + + +// ============================================================================================================================== +// Training Area Configurations +// ============================================================================================================================== + + +/** + * The maximum allowed area size (in square meters) for training areas. + */ +export const MAX_TRAINING_AREA_SIZE: number = parseIntEnv(ENVS.MAX_TRAINING_AREA_SIZE, 5000000); + +/** + * The minimum allowed area size (in square meters) for training areas. + */ +export const MIN_TRAINING_AREA_SIZE: number = parseIntEnv(ENVS.MIN_TRAINING_AREA_SIZE, 5797); + +/** + * The maximum file size (in bytes) allowed for training area upload. + * The default is set to 5 MB. + */ +export const MAX_TRAINING_AREA_UPLOAD_FILE_SIZE: number = parseIntEnv(ENVS.MAX_TRAINING_AREA_UPLOAD_FILE_SIZE, 5 * 1024 * 1024); + +/** + * The maximum GeoJSON file(s) containing the training labels, a user can upload for an AOI/Training Area. + * Default value: 1 (1 GeoJSON file). +*/ +export const MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREA_LABELS: number = parseIntEnv(ENVS.MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREA_LABELS, 1); + +/** + * The maximum GeoJSON file(s) containing the training areas/AOI polygon geometry that a user can upload. + * Default value: 10 (10 GeoJSON files, assumming each file has a single AOI). +*/ +export const MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREAS: number = parseIntEnv(ENVS.MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREAS, 10); + +/** + * The maximum polygon geometry a single training area GeoJSON file can contain. + * Default value: 10 (10 polygon geometries). +*/ +export const MAX_ACCEPTABLE_POLYGON_IN_TRAINING_AREA_GEOJSON_FILE: number = parseIntEnv(ENVS.MAX_ACCEPTABLE_POLYGON_IN_TRAINING_AREA_GEOJSON_FILE, 10); + + + +// ============================================================================================================================== +// Map Configurations +// ============================================================================================================================== + + + +/** + * The maximum zoom level for the map. + */ +export const MAX_ZOOM_LEVEL: number = parseIntEnv(ENVS.MAX_ZOOM_LEVEL, 22); + +/** + * The minimum zoom level for the map before the prediction components can be activated. + */ +export const MIN_ZOOM_LEVEL_FOR_START_MAPPING_PREDICTION: number = parseIntEnv(ENVS.MIN_ZOOM_LEVEL_FOR_START_MAPPING_PREDICTION, 19); + +/** + * The instruction to show the users when they haven't reach the minimum zoom level on the start mapping page. + */ +export const MINIMUM_ZOOM_LEVEL_INSTRUCTION_FOR_PREDICTION: string = `Zoom in to at least zoom ${MIN_ZOOM_LEVEL_FOR_START_MAPPING_PREDICTION} to start mapping.`; + +/** + * A unique ID to append to all custom map sources and layers ids. This is useful for the legend component to dynamically get the layers on the map excluding the basemaps styles. + */ +export const MAP_STYLES_PREFIX: string = "fAIr"; + +/** + * The minimum zoom level to show the training area labels. + */ +export const MIN_ZOOM_LEVEL_FOR_TRAINING_AREA_LABELS: number = parseIntEnv(ENVS.MIN_ZOOM_LEVEL_FOR_TRAINING_AREA_LABELS, 18); + +/** + * OSM Basemap style. +*/ +export const MAP_STYLES: Record = { + OSM: "https://tiles.openfreemap.org/styles/bright", +}; + + + +// ============================================================================================================================== +// Layers, Sources and Name Mappings +// ============================================================================================================================== + + + +// Shared (Basemaps, Tile Boundaries) +export const TILE_BOUNDARY_LAYER_ID: string = `${MAP_STYLES_PREFIX}-tile-boundary-layer`; +export const TILE_BOUNDARY_SOURCE_ID: string = `${MAP_STYLES_PREFIX}-tile-boundaries`; +export const TMS_LAYER_ID: string = `${MAP_STYLES_PREFIX}-oam-tms-layer`; +export const TMS_SOURCE_ID: string = `${MAP_STYLES_PREFIX}-oam-training-dataset`; +export const OSM_BASEMAP_LAYER_ID: string = `${MAP_STYLES_PREFIX}-osm-layer`; +export const GOOGLE_SATELLITE_BASEMAP_LAYER_ID: string = `${MAP_STYLES_PREFIX}-google-statellite-layer`; +export const GOOGLE_SATELLITE_BASEMAP_SOURCE_ID: string = `${MAP_STYLES_PREFIX}-google-satellite`; + +// Start Mapping +export const ACCEPTED_MODEL_PREDICTIONS_SOURCE_ID: string = "accepted-predictions-source"; +export const ACCEPTED_MODEL_PREDICTIONS_FILL_LAYER_ID: string = `${MAP_STYLES_PREFIX}-accepted-predictions-fill-layer`; +export const ACCEPTED_MODEL_PREDICTIONS_OUTLINE_LAYER_ID: string = "accepted-predictions-outline-layer"; +export const ALL_MODEL_PREDICTIONS_SOURCE_ID: string = "all-predictions-source"; +export const ALL_MODEL_PREDICTIONS_FILL_LAYER_ID: string = `${MAP_STYLES_PREFIX}-all-predictions-fill-layer`; +export const ALL_MODEL_PREDICTIONS_OUTLINE_LAYER_ID: string = "all-predictions-outline-layer"; +export const REJECTED_MODEL_PREDICTIONS_SOURCE_ID: string = "rejected-predictions-source"; +export const REJECTED_MODEL_PREDICTIONS_FILL_LAYER_ID: string = `${MAP_STYLES_PREFIX}-rejected-predictions-fill-layer`; +export const REJECTED_MODEL_PREDICTIONS_OUTLINE_LAYER_ID: string = "rejected-predictions-outline-layer"; + +// Training Areas +export const TRAINING_AREAS_AOI_FILL_COLOR: string = parseStringEnv(ENVS.TRAINING_AREAS_AOI_FILL_COLOR, "#247DCACC"); +export const TRAINING_AREAS_AOI_OUTLINE_COLOR: string = parseStringEnv(ENVS.TRAINING_AREAS_AOI_OUTLINE_COLOR, "#247DCACC"); +export const TRAINING_AREAS_AOI_OUTLINE_WIDTH: number = parseIntEnv(ENVS.TRAINING_AREAS_AOI_OUTLINE_WIDTH, 4); +export const TRAINING_AREAS_AOI_FILL_OPACITY: number = parseFloatEnv(ENVS.TRAINING_AREAS_AOI_FILL_OPACITY, 0.4); +export const TRAINING_AREAS_AOI_LABELS_FILL_OPACITY: number = parseFloatEnv(ENVS.TRAINING_AREAS_AOI_LABELS_FILL_OPACITY, 0.3); +export const TRAINING_AREAS_AOI_LABELS_OUTLINE_WIDTH: number = parseIntEnv(ENVS.TRAINING_AREAS_AOI_LABELS_OUTLINE_WIDTH, 2); +export const TRAINING_AREAS_AOI_LABELS_FILL_COLOR: string = parseStringEnv(ENVS.TRAINING_AREAS_AOI_LABELS_FILL_COLOR, "#D73434"); +export const TRAINING_AREAS_AOI_LABELS_OUTLINE_COLOR: string = parseStringEnv(ENVS.TRAINING_AREAS_AOI_LABELS_OUTLINE_COLOR, "#D73434"); + +// Start Mapping Legend - only the fill layers are in the legend. +export const LEGEND_NAME_MAPPING: Record = { + [ALL_MODEL_PREDICTIONS_FILL_LAYER_ID]: "Map Result", + [REJECTED_MODEL_PREDICTIONS_FILL_LAYER_ID]: "Rejected", + [ACCEPTED_MODEL_PREDICTIONS_FILL_LAYER_ID]: "Accepted", +}; + + + +// ============================================================================================================================== +// Others +// ============================================================================================================================== + + +/** + * The web component tag name used in `hotosm/ui` for the tracking component. + */ +export const HOT_TRACKING_HTML_TAG_NAME: string = "hot-tracking"; + +/** + * The matomo application ID. + * Default value: "0". + * Matomo will be used as an attribute in the hot-tracking component, so we need to pass it as string to the component. + */ +export const MATOMO_ID: string = parseStringEnv(ENVS.MATOMO_ID, "0"); + +/** + * The matomo application domain. + */ +export const MATOMO_APP_DOMAIN: string = parseStringEnv(ENVS.MATOMO_APP_DOMAIN, "fair.hotosm.org"); + +/** + * The file extensions for the prediction api. + */ +export const PREDICTION_API_FILE_EXTENSIONS: Record = { + [BASE_MODELS.RAMP]: ".tflite", + [BASE_MODELS.YOLOV8_V1]: ".onnx", + [BASE_MODELS.YOLOV8_V2]: ".onnx", +}; + +/** + * The time to poll the backend for the status of the AOI training labels fetching, in milliseconds (ms). + * Default value: 5000 ms (5 seconds). + */ +export const TRAINING_AREA_LABELS_FETCH_POOLING_TIME_MS: number = parseIntEnv(ENVS.TRAINING_AREA_LABELS_FETCH_POOLING_INTERVAL_MS, 5000); + +/** + * The time to poll the backend for the status of the OSM last updated time, in milliseconds (ms). + * Default value: 10000 (ms i.e 10 seconds). + */ +export const OSM_LAST_UPDATED_POOLING_INTERVAL_MS: number = parseIntEnv(ENVS.OSM_LAST_UPDATED_POOLING_INTERVAL_MS, 10000); + + +/** + * The current version of the application. + * This is used in the OSM redirect callback when a training area is opened in OSM. + */ +export const FAIR_VERSION: string = parseStringEnv(ENVS.FAIR_VERSION, "v0.1"); + +/** + * Comma separated hashtags to add to the OSM ID Editor redirection. + * This is used in the OSM redirect callback when a training area is opened in OSM. + */ +export const OSM_HASHTAGS: string = parseStringEnv(ENVS.OSM_HASHTAGS, "#HOT-fAIr"); + +/** + * Configuration for KPI Statistics Refetching Interval. + */ + +// Default cache time in seconds (15 minutes) +const DEFAULT_KPI_STATS_CACHE_TIME_SECONDS: number = 900; + +// Buffer time in milliseconds (1 second) +const REFRESH_BUFFER_MS: number = 1000; + +/** + * The cache time to poll the backend for updated KPI statistics, in milliseconds. + * It includes an additional buffer to ensure fresh data retrieval. + */ +export const KPI_STATS_CACHE_TIME_MS: number = parseIntEnv(ENVS.KPI_STATS_CACHE_TIME, DEFAULT_KPI_STATS_CACHE_TIME_SECONDS) * 1000 + REFRESH_BUFFER_MS; + + + +// ============================================================================================================================== +// UI Settings +// ============================================================================================================================== + + + +/** + * Distance of the elements from the navbar in px for dropdowns and popups on the start mapping page. + */ +export const ELEMENT_DISTANCE_FROM_NAVBAR: number = 10; diff --git a/frontend/src/constants/config.ts b/frontend/src/constants/config.ts deleted file mode 100644 index e7a5a8ed..00000000 --- a/frontend/src/constants/config.ts +++ /dev/null @@ -1,269 +0,0 @@ -import { BASE_MODELS } from '@/enums'; -import { ENVS } from '@/config/env'; -import { StyleSpecification } from 'maplibre-gl'; - -/** - * The key used to store the access token in local storage for the application. - */ -export const HOT_FAIR_LOCAL_STORAGE_ACCESS_TOKEN_KEY: string = - "___hot_fAIr_access_token"; - -/** - * The key used to store the redirect URL after login in session storage for the application. - */ -export const HOT_FAIR_SESSION_REDIRECT_KEY: string = - "___hot_fAIr_redirect_after_login"; - -/** - * The key used to indicate a successful login session for the application. - */ -export const HOT_FAIR_LOGIN_SUCCESSFUL_SESSION_KEY = - "__hot_fair_login_successful"; - -/** - * Configuration for KPI Statistics Refetching Interval. - */ - -// Default cache time in seconds (15 minutes) -const DEFAULT_KPI_STATS_CACHE_TIME_SECONDS = 900; - -// Buffer time in milliseconds (1 second) -const REFRESH_BUFFER_MS = 1000; - -/** - * The cache time to poll the backend for updated KPI statistics, in milliseconds. - * It includes an additional buffer to ensure fresh data retrieval. - * - * @type {number} - */ -export const KPI_STATS_CACHE_TIME_MS = - (Number(ENVS.KPI_STATS_CACHE_TIME) || DEFAULT_KPI_STATS_CACHE_TIME_SECONDS) * - 1000 + - REFRESH_BUFFER_MS; - -/** - * The maximum allowed area size (in square meters) for training areas. - */ -export const MAX_TRAINING_AREA_SIZE = ENVS.MAX_TRAINING_AREA_SIZE || 5000000; - -/** - * The minimum allowed area size (in square meters) for training areas. - */ -export const MIN_TRAINING_AREA_SIZE = ENVS.MIN_TRAINING_AREA_SIZE || 5797; - -/** - * The maximum file size (in bytes) allowed for training area upload. - * The default is set to 5 MB. - */ -export const MAX_TRAINING_AREA_UPLOAD_FILE_SIZE = - ENVS.MAX_TRAINING_AREA_UPLOAD_FILE_SIZE || 5 * 1024 * 1024; - -/** - * The current version of the application. - * This is used in the OSM redirect callback when a training area is opened in OSM. - */ -export const FAIR_VERSION = ENVS.FAIR_VERSION || "v0.1"; - -/** - * The current version of the application. - * This is used in the OSM redirect callback when a training area is opened in OSM. - */ -export const OSM_HASHTAGS = ENVS.OSM_HASHTAGS || FAIR_VERSION; - -/** - * The maximum zoom level for the map. - */ -export const MAX_ZOOM_LEVEL = ENVS.MAX_ZOOM_LEVEL || 22; - -/** - * The minimum zoom level for the map before the prediction components can be activated. - */ -export const MIN_ZOOM_LEVEL_FOR_START_MAPPING_PREDICTION = - ENVS.MIN_ZOOM_LEVEL_FOR_START_MAPPING_PREDICTION || 19; - -/** - * The instruction to show the users when they haven't reach the minimum zoom level on the start mapping page. - */ -export const MINIMUM_ZOOM_LEVEL_INSTRUCTION_FOR_PREDICTION = `Zoom in to at least zoom ${MIN_ZOOM_LEVEL_FOR_START_MAPPING_PREDICTION} to start mapping.`; - -/** - * A unique ID to append to all custom map sources and layers ids. This is useful for the legend component to dynamically get the layers on the map excluding the basemaps styles. - */ - -export const MAP_STYLES_PREFIX = "fAIr"; -/** - * The minimum zoom level to show the training area labels. - */ -export const MIN_ZOOM_LEVEL_FOR_TRAINING_AREA_LABELS = - ENVS.MIN_ZOOM_LEVEL_FOR_TRAINING_AREA_LABELS || 18; - -// Layers, Sources and Name Mappings - -export const TILE_BOUNDARY_LAYER_ID = `${MAP_STYLES_PREFIX}-tile-boundary-layer`; -export const TILE_BOUNDARY_SOURCE_ID = `${MAP_STYLES_PREFIX}-tile-boundaries`; -export const TMS_LAYER_ID = `${MAP_STYLES_PREFIX}-oam-tms-layer`; -export const TMS_SOURCE_ID = `${MAP_STYLES_PREFIX}-oam-training-dataset`; -export const OSM_BASEMAP_LAYER_ID = `${MAP_STYLES_PREFIX}-osm-layer`; -export const GOOGLE_SATELLITE_BASEMAP_LAYER_ID = `${MAP_STYLES_PREFIX}-google-statellite-layer`; -export const GOOGLE_SATELLITE_BASEMAP_SOURCE_ID = `${MAP_STYLES_PREFIX}-google-satellite`; - -// Model Predictions - -// accepted - -export const ACCEPTED_MODEL_PREDICTIONS_SOURCE_ID = - "accepted-predictions-source"; -export const ACCEPTED_MODEL_PREDICTIONS_FILL_LAYER_ID = `${MAP_STYLES_PREFIX}-accepted-predictions-fill-layer`; -export const ACCEPTED_MODEL_PREDICTIONS_OUTLINE_LAYER_ID = - "accepted-predictions-outline-layer"; - -// all - -export const ALL_MODEL_PREDICTIONS_SOURCE_ID = "all-predictions-source"; -export const ALL_MODEL_PREDICTIONS_FILL_LAYER_ID = `${MAP_STYLES_PREFIX}-all-predictions-fill-layer`; -export const ALL_MODEL_PREDICTIONS_OUTLINE_LAYER_ID = - "all-predictions-outline-layer"; - -// rejected -export const REJECTED_MODEL_PREDICTIONS_SOURCE_ID = - "rejected-predictions-source"; -export const REJECTED_MODEL_PREDICTIONS_FILL_LAYER_ID = `${MAP_STYLES_PREFIX}-rejected-predictions-fill-layer`; -export const REJECTED_MODEL_PREDICTIONS_OUTLINE_LAYER_ID = - "rejected-predictions-outline-layer"; - -// Legend is only used on the start mapping page -// and only the fill layers are in the legend. - -export const LEGEND_NAME_MAPPING: Record = { - [ALL_MODEL_PREDICTIONS_FILL_LAYER_ID]: "Map Result", - [REJECTED_MODEL_PREDICTIONS_FILL_LAYER_ID]: "Rejected", - [ACCEPTED_MODEL_PREDICTIONS_FILL_LAYER_ID]: "Accepted", -}; - -/** - * Training area and labels styles. - */ -export const TRAINING_AREAS_AOI_FILL_COLOR = - ENVS.TRAINING_AREAS_AOI_FILL_COLOR || "#247DCACC"; -export const TRAINING_AREAS_AOI_OUTLINE_COLOR = - ENVS.TRAINING_AREAS_AOI_OUTLINE_COLOR || "#247DCACC"; -export const TRAINING_AREAS_AOI_OUTLINE_WIDTH = - ENVS.TRAINING_AREAS_AOI_OUTLINE_WIDTH || 4; -export const TRAINING_AREAS_AOI_FILL_OPACITY = - ENVS.TRAINING_AREAS_AOI_FILL_OPACITY || 0.4; -export const TRAINING_AREAS_AOI_LABELS_FILL_OPACITY = - ENVS.TRAINING_AREAS_AOI_LABELS_FILL_OPACITY || 0.3; -export const TRAINING_AREAS_AOI_LABELS_OUTLINE_WIDTH = - ENVS.TRAINING_AREAS_AOI_LABELS_OUTLINE_WIDTH || 2; -export const TRAINING_AREAS_AOI_LABELS_FILL_COLOR = - ENVS.TRAINING_AREAS_AOI_LABELS_FILL_COLOR || "#D73434"; -export const TRAINING_AREAS_AOI_LABELS_OUTLINE_COLOR = - ENVS.TRAINING_AREAS_AOI_LABELS_OUTLINE_COLOR || "#D73434"; - - -/** - * The key used to store the model form data in session storage to preserve the state incase the user - * visits ID Editor or JOSM to map a training area. - * Session storage is used to allow users to be able to open fAIr on a new tab and start on a clean slate. - */ -export const HOT_FAIR_MODEL_CREATION_SESSION_STORAGE_KEY = "__hot_fair_model_creation_formdata"; - - -/** - * The key used to store the banner state in local storage for the application. - */ -export const HOT_FAIR_BANNER_LOCAL_STORAGE_KEY = "__hot_fair_banner_closed"; - -/** - * The key used to store the model predictions in the session storage for the application. - */ -export const HOT_FAIR_MODEL_PREDICTIONS_SESSION_STORAGE_KEY = - "__hot_fair_model_predictions"; - -// MAP SETTINGS - -export const MAP_STYLES: Record = { - // ref - https://openfreemap.org/ - OSM: "https://tiles.openfreemap.org/styles/bright", -}; - -/** - * The web component tag name used in `hotosm/ui` for the tracking component. - */ -export const HOT_TRACKING_HTML_TAG_NAME = "hot-tracking"; - -/** - * The file extension for the prediction api. - */ - -export const PREDICTION_API_FILE_EXTENSIONS = { - [BASE_MODELS.RAMP]: ".tflite", - [BASE_MODELS.YOLOV8_V1]: ".onnx", - [BASE_MODELS.YOLOV8_V2]: ".onnx", -}; - -/** - * The remote url to JOSM. - */ -export const JOSM_REMOTE_URL = ENVS.JOSM_REMOTE_URL || "http://127.0.0.1:8111/"; - -/** - * The time to poll the backend for the status of the AOI training labels fetching, in milliseconds (ms). - * Defaults to 5000 ms (5 seconds). - */ -export const TRAINING_AREA_LABELS_FETCH_POOLING_TIME_MS = - ENVS.TRAINING_AREA_LABELS_FETCH_POOLING_INTERVAL_MS || 5000; - -/** - * The time to poll the backend for the status of the OSM last updated time, in milliseconds (ms). - * Data type: Positive Integer (e.g., 900). - * Default value: 10000 milliseconds (10 seconds). - */ -export const OSM_LAST_UPDATED_POOLING_INTERVAL_MS = - ENVS.OSM_LAST_UPDATED_POOLING_INTERVAL_MS || 10000; - -/** - * Distance of the elements from the navbar in px for dropdowns and popups on the start mapping page. - */ -export const ELEMENT_DISTANCE_FROM_NAVBAR = 10; - -/** - The maximum GeoJSON file(s) containing the training labels, a user can upload for an AOI/Training Area. - Data type: Positive Integer (e.g., 1). - Default value: 1 (1 GeoJSON file). -*/ -export const MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREA_LABELS = - ENVS.MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREA_LABELS || 1; - -/** - The maximum GeoJSON file(s) containing the training areas/AOI polygon geometry that a user can upload. - Data type: Positive Integer (e.g., 1). - Default value: 10 (10 GeoJSON files, assumming each file has a single AOI). -*/ -export const MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREAS = - ENVS.MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREAS || 10; - -/** - The maximum polygon geometry a single training area GeoJSON file can contain. - Data type: Positive Integer (e.g., 1). - Default value: 10 (10 polygon geometries). -*/ -export const MAX_ACCEPTABLE_POLYGON_IN_TRAINING_AREA_GEOJSON_FILE = - ENVS.MAX_ACCEPTABLE_POLYGON_IN_TRAINING_AREA_GEOJSON_FILE || 10; - - -/** - The Base URL for OAM's Titiler. - Data type: String (e.g.,https://titiler.hotosm.org/). - Default value: https://titiler.hotosm.org/. -*/ -export const OAM_TITILER_ENDPOINT = ENVS.OAM_TITILER_ENDPOINT || "https://titiler.hotosm.org/"; - - - -/** - The new S3 bucket for OAM aerial imageries. - Data type: String (e.g.,https://oin-hotosm-temp.s3.us-east-1.amazonaws.com/). - Default value: https://oin-hotosm-temp.s3.us-east-1.amazonaws.com/. -*/ -export const OAM_S3_BUCKET_URL = ENVS.OAM_S3_BUCKET_URL || "https://oin-hotosm-temp.s3.us-east-1.amazonaws.com/"; diff --git a/frontend/src/constants/index.ts b/frontend/src/constants/index.ts index f857cef7..067b4efe 100644 --- a/frontend/src/constants/index.ts +++ b/frontend/src/constants/index.ts @@ -2,4 +2,3 @@ export * from "./error-messages"; export * from "./ui-contents"; export * from "./toast-notifications"; export * from "./routes"; -export * from "./config"; diff --git a/frontend/src/constants/ui-contents/models-content.ts b/frontend/src/constants/ui-contents/models-content.ts index 1fb22874..b8f8d803 100644 --- a/frontend/src/constants/ui-contents/models-content.ts +++ b/frontend/src/constants/ui-contents/models-content.ts @@ -1,8 +1,9 @@ import { BASE_MODELS } from '@/enums'; import { formatAreaInAppropriateUnit } from '@/utils'; -import { MAX_TRAINING_AREA_SIZE, MIN_TRAINING_AREA_SIZE } from '../config'; +import { MAX_TRAINING_AREA_SIZE, MIN_TRAINING_AREA_SIZE } from '@/config'; import { TModelsContent } from '@/types'; + export const MODELS_CONTENT: TModelsContent = { trainingArea: { // The retry button when the training area map fails to load as a result of an API error or any other issue. diff --git a/frontend/src/features/model-creation/components/dialogs/file-upload-dialog.tsx b/frontend/src/features/model-creation/components/dialogs/file-upload-dialog.tsx index fd447ab6..d63b83fd 100644 --- a/frontend/src/features/model-creation/components/dialogs/file-upload-dialog.tsx +++ b/frontend/src/features/model-creation/components/dialogs/file-upload-dialog.tsx @@ -1,19 +1,20 @@ -import { Button } from "@/components/ui/button"; -import { DeleteIcon, FileIcon, UploadIcon } from "@/components/ui/icons"; -import { Dialog } from "@/components/ui/dialog"; -import { FileWithPath, useDropzone } from "react-dropzone"; -import { Geometry, MultiPolygon, Polygon } from "geojson"; -import { SlFormatBytes } from "@shoelace-style/shoelace/dist/react"; -import { Spinner } from "@/components/ui/spinner"; -import { useCallback, useState } from "react"; -import { DialogProps, Feature, FeatureCollection } from "@/types"; +import { Button } from '@/components/ui/button'; +import { DeleteIcon, FileIcon, UploadIcon } from '@/components/ui/icons'; +import { Dialog } from '@/components/ui/dialog'; +import { DialogProps, Feature, FeatureCollection } from '@/types'; +import { FileWithPath, useDropzone } from 'react-dropzone'; +import { Geometry, MultiPolygon, Polygon } from 'geojson'; +import { MODELS_CONTENT } from '@/constants'; +import { SlFormatBytes } from '@shoelace-style/shoelace/dist/react'; +import { Spinner } from '@/components/ui/spinner'; +import { useCallback, useState } from 'react'; import { MAX_ACCEPTABLE_POLYGON_IN_TRAINING_AREA_GEOJSON_FILE, MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREA_LABELS, MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREAS, MAX_TRAINING_AREA_UPLOAD_FILE_SIZE, - MODELS_CONTENT, -} from "@/constants"; +} from "@/config"; + import { formatAreaInAppropriateUnit, showErrorToast, @@ -140,7 +141,7 @@ const FileUploadDialog: React.FC = ({ disabled || uploadInProgress || acceptedFiles.length === - MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREA_LABELS || + MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREA_LABELS || acceptedFiles.length === MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREAS, }); diff --git a/frontend/src/features/model-creation/components/training-area/training-area-item.tsx b/frontend/src/features/model-creation/components/training-area/training-area-item.tsx index 2ec9197d..eb9ab866 100644 --- a/frontend/src/features/model-creation/components/training-area/training-area-item.tsx +++ b/frontend/src/features/model-creation/components/training-area/training-area-item.tsx @@ -5,12 +5,13 @@ import { JOSMLogo, OSMLogo } from '@/assets/svgs'; import { LabelStatus } from '@/enums/training-area'; import { Map } from 'maplibre-gl'; import { ToolTip } from '@/components/ui/tooltip'; +import { TRAINING_AREA_LABELS_FETCH_POOLING_TIME_MS } from '@/config'; import { useCallback, useEffect, useRef, useState -} from 'react'; + } from 'react'; import { useDialog } from '@/hooks/use-dialog'; import { useDropdownMenu } from '@/hooks/use-dropdown-menu'; import { useModelsContext } from '@/app/providers/models-provider'; @@ -38,7 +39,6 @@ import { import { MODELS_CONTENT, TOAST_NOTIFICATIONS, - TRAINING_AREA_LABELS_FETCH_POOLING_TIME_MS, } from "@/constants"; import { useCreateTrainingLabelsForAOI, diff --git a/frontend/src/features/model-creation/components/training-area/training-area-list.tsx b/frontend/src/features/model-creation/components/training-area/training-area-list.tsx index a9093090..be8913e9 100644 --- a/frontend/src/features/model-creation/components/training-area/training-area-list.tsx +++ b/frontend/src/features/model-creation/components/training-area/training-area-list.tsx @@ -1,14 +1,14 @@ -import { Dispatch, SetStateAction } from "react"; -import { fetchOSMDatabaseLastUpdated } from "@/features/model-creation/hooks/use-training-areas"; -import { formatDuration } from "@/utils"; -import { Map } from "maplibre-gl"; -import { NoTrainingAreaIcon } from "@/components/ui/icons"; -import { PaginatedTrainingArea } from "@/types"; -import { Pagination } from "@/components/shared"; -import { TrainingAreaItem } from "@/features/model-creation/components/training-area/training-area-item"; -import { useQuery } from "@tanstack/react-query"; +import { Dispatch, SetStateAction } from 'react'; +import { fetchOSMDatabaseLastUpdated } from '@/features/model-creation/hooks/use-training-areas'; +import { formatDuration } from '@/utils'; +import { Map } from 'maplibre-gl'; +import { NoTrainingAreaIcon } from '@/components/ui/icons'; +import { OSM_LAST_UPDATED_POOLING_INTERVAL_MS } from '@/config'; +import { PaginatedTrainingArea } from '@/types'; +import { Pagination } from '@/components/shared'; +import { TrainingAreaItem } from '@/features/model-creation/components/training-area/training-area-item'; +import { useQuery } from '@tanstack/react-query'; import { - OSM_LAST_UPDATED_POOLING_INTERVAL_MS, MODELS_CONTENT, } from "@/constants"; diff --git a/frontend/src/features/model-creation/components/training-area/training-area-map.tsx b/frontend/src/features/model-creation/components/training-area/training-area-map.tsx index 6343620e..759de381 100644 --- a/frontend/src/features/model-creation/components/training-area/training-area-map.tsx +++ b/frontend/src/features/model-creation/components/training-area/training-area-map.tsx @@ -1,14 +1,19 @@ -import useDebounce from "@/hooks/use-debounce"; -import { ControlsPosition, DrawingModes } from "@/enums"; -import { GeoJSONSource, Map } from "maplibre-gl"; -import { geojsonToWKT } from "@terraformer/wkt"; -import { GeoJSONType, PaginatedTrainingArea } from "@/types"; -import { MapComponent, MapCursorToolTip } from "@/components/map"; -import { Polygon } from "geojson"; -import { RefObject, useCallback, useEffect, useState } from "react"; -import { TerraDraw } from "terra-draw"; -import { useMapLayers } from "@/hooks/use-map-layer"; -import { useToolTipVisibility } from "@/hooks/use-tooltip-visibility"; +import useDebounce from '@/hooks/use-debounce'; +import { ControlsPosition, DrawingModes } from '@/enums'; +import { GeoJSONSource, Map } from 'maplibre-gl'; +import { geojsonToWKT } from '@terraformer/wkt'; +import { GeoJSONType, PaginatedTrainingArea } from '@/types'; +import { MapComponent, MapCursorToolTip } from '@/components/map'; +import { Polygon } from 'geojson'; +import { + RefObject, + useCallback, + useEffect, + useState + } from 'react'; +import { TerraDraw } from 'terra-draw'; +import { useMapLayers } from '@/hooks/use-map-layer'; +import { useToolTipVisibility } from '@/hooks/use-tooltip-visibility'; import { useCreateTrainingArea, useGetTrainingDatasetLabels, @@ -26,7 +31,7 @@ import { TRAINING_AREAS_AOI_OUTLINE_COLOR, TRAINING_AREAS_AOI_OUTLINE_WIDTH, MIN_ZOOM_LEVEL_FOR_TRAINING_AREA_LABELS, -} from "@/constants"; +} from "@/config"; import { calculateGeoJSONArea, formatAreaInAppropriateUnit, @@ -312,22 +317,22 @@ const TrainingAreaMap = ({ layerControlLayers={[ ...(data?.results?.features?.length ? [ - { - value: "Training Areas", - subLayers: [trainingAreasLayerId, trainingAreasFillLayerId], - }, - ] + { + value: "Training Areas", + subLayers: [trainingAreasLayerId, trainingAreasFillLayerId], + }, + ] : []), ...(labels && labels?.features.length > 0 ? [ - { - value: "Training Labels", - subLayers: [ - trainingDatasetLabelsLayerId, - trainingDatasetLabelsOutlineLayerId, - ], - }, - ] + { + value: "Training Labels", + subLayers: [ + trainingDatasetLabelsLayerId, + trainingDatasetLabelsOutlineLayerId, + ], + }, + ] : []), ]} > diff --git a/frontend/src/features/model-creation/hooks/use-training-areas.ts b/frontend/src/features/model-creation/hooks/use-training-areas.ts index 51d3c49f..5b23641f 100644 --- a/frontend/src/features/model-creation/hooks/use-training-areas.ts +++ b/frontend/src/features/model-creation/hooks/use-training-areas.ts @@ -1,7 +1,7 @@ import axios from 'axios'; import { API_ENDPOINTS, MutationConfig } from '@/services'; import { deleteTrainingArea } from '@/features/model-creation/api/delete-trainings'; -import { MIN_ZOOM_LEVEL_FOR_TRAINING_AREA_LABELS } from '@/constants'; +import { MIN_ZOOM_LEVEL_FOR_TRAINING_AREA_LABELS } from '@/config'; import { useMutation, useQuery } from '@tanstack/react-query'; import { getTrainingAreaLabelsQueryOptions, diff --git a/frontend/src/features/models/components/maps/training-area-map.tsx b/frontend/src/features/models/components/maps/training-area-map.tsx index 422c9b8c..bf078108 100644 --- a/frontend/src/features/models/components/maps/training-area-map.tsx +++ b/frontend/src/features/models/components/maps/training-area-map.tsx @@ -1,8 +1,14 @@ -import { ControlsPosition } from "@/enums"; -import { MapComponent } from "@/components/map"; -import { PMTiles } from "pmtiles"; -import { useCallback, useEffect, useRef, useState } from "react"; -import { useMapInstance } from "@/hooks/use-map-instance"; +import { ControlsPosition } from '@/enums'; +import { errorMessages } from '@/constants'; +import { MapComponent } from '@/components/map'; +import { PMTiles } from 'pmtiles'; +import { + useCallback, + useEffect, + useRef, + useState + } from 'react'; +import { useMapInstance } from '@/hooks/use-map-instance'; import { LayerSpecification, MapLayerMouseEvent, @@ -25,8 +31,8 @@ import { TRAINING_AREAS_AOI_LABELS_OUTLINE_WIDTH, TRAINING_AREAS_AOI_OUTLINE_COLOR, TRAINING_AREAS_AOI_OUTLINE_WIDTH, - errorMessages, -} from "@/constants"; + +} from "@/config"; type Metadata = { name?: string; @@ -172,15 +178,15 @@ export const TrainingAreaMap = ({ ${Object.entries(feature.properties) - .map( - ([key, value]) => ` + .map( + ([key, value]) => ` `, - ) - .join("")} + ) + .join("")}
${key} ${typeof value === "boolean" ? JSON.stringify(value) : value}
diff --git a/frontend/src/features/models/components/model-details-properties.tsx b/frontend/src/features/models/components/model-details-properties.tsx index f2a6f0c3..bce0f3ca 100644 --- a/frontend/src/features/models/components/model-details-properties.tsx +++ b/frontend/src/features/models/components/model-details-properties.tsx @@ -1,21 +1,21 @@ -import AccuracyDisplay from "./accuracy-display"; -import CodeBlock from "@/components/ui/codeblock/codeblock"; -import ModelFilesButton from "./model-files-button"; -import ToolTip from "@/components/ui/tooltip/tooltip"; -import useCopyToClipboard from "@/hooks/use-clipboard"; -import { ChevronDownIcon } from "@/components/ui/icons"; -import { cn, showErrorToast } from "@/utils"; -import { CopyIcon, ExternalLinkIcon } from "@/components/ui/icons"; -import { ENVS } from "@/config/env"; -import { Image, ZoomableImage } from "@/components/ui/image"; -import { Link } from "@/components/ui/link"; -import { ModelFilesDialog } from "./dialogs"; -import { ModelPropertiesSkeleton } from "./skeletons"; -import { MODELS_CONTENT } from "@/constants"; -import { TrainingAreaButton } from "./training-area-button"; -import { TrainingAreaDrawer } from "./training-area-drawer"; -import { useDialog } from "@/hooks/use-dialog"; -import { useEffect, useState } from "react"; +import AccuracyDisplay from './accuracy-display'; +import CodeBlock from '@/components/ui/codeblock/codeblock'; +import ModelFilesButton from './model-files-button'; +import ToolTip from '@/components/ui/tooltip/tooltip'; +import useCopyToClipboard from '@/hooks/use-clipboard'; +import { BASE_API_URL } from '@/config'; +import { ChevronDownIcon } from '@/components/ui/icons'; +import { cn, showErrorToast } from '@/utils'; +import { CopyIcon, ExternalLinkIcon } from '@/components/ui/icons'; +import { Image, ZoomableImage } from '@/components/ui/image'; +import { Link } from '@/components/ui/link'; +import { ModelFilesDialog } from './dialogs'; +import { ModelPropertiesSkeleton } from './skeletons'; +import { MODELS_CONTENT } from '@/constants'; +import { TrainingAreaButton } from './training-area-button'; +import { TrainingAreaDrawer } from './training-area-drawer'; +import { useDialog } from '@/hooks/use-dialog'; +import { useEffect, useState } from 'react'; import { useTrainingDetails, useTrainingStatus, @@ -139,7 +139,7 @@ const ModelProperties: React.FC = ({ chips_length, } = data || {}; - const trainingResultsGraph = `${ENVS.BASE_API_URL}workspace/download/training_${data?.id}/graphs/training_accuracy.png`; + const trainingResultsGraph = `${BASE_API_URL}workspace/download/training_${data?.id}/graphs/training_accuracy.png`; return isError || isPending ? ( diff --git a/frontend/src/features/start-mapping/components/header.tsx b/frontend/src/features/start-mapping/components/header.tsx index bc2ac654..4960972d 100644 --- a/frontend/src/features/start-mapping/components/header.tsx +++ b/frontend/src/features/start-mapping/components/header.tsx @@ -1,24 +1,26 @@ -import ModelAction from "@/features/start-mapping/components/model-action"; -import { BrandLogoWithDropDown } from "./logo-with-dropdown"; -import { ButtonWithIcon } from "@/components/ui/button"; -import { ChevronDownIcon } from "@/components/ui/icons"; -import { DropDown } from "@/components/ui/dropdown"; -import { DropdownPlacement, SHOELACE_SIZES } from "@/enums"; -import { Map } from "maplibre-gl"; -import { ModelDetailsButton } from "@/features/start-mapping/components/model-details-button"; -import { ModelPredictionsTracker } from "@/features/start-mapping/components/model-predictions-tracker"; -import { ModelSettings } from "@/features/start-mapping/components/model-settings"; -import { SkeletonWrapper } from "@/components/ui/skeleton"; -import { TDownloadOptions, TQueryParams } from "@/app/routes/start-mapping"; -import { TModel, TModelPredictions, TModelPredictionsConfig } from "@/types"; -import { ToolTip } from "@/components/ui/tooltip"; -import { useDropdownMenu } from "@/hooks/use-dropdown-menu"; -import { UserProfile } from "@/components/layout"; +import ModelAction from '@/features/start-mapping/components/model-action'; +import { BrandLogoWithDropDown } from './logo-with-dropdown'; +import { ButtonWithIcon } from '@/components/ui/button'; +import { ChevronDownIcon } from '@/components/ui/icons'; +import { DropDown } from '@/components/ui/dropdown'; +import { DropdownPlacement, SHOELACE_SIZES } from '@/enums'; +import { ELEMENT_DISTANCE_FROM_NAVBAR } from '@/config'; +import { Map } from 'maplibre-gl'; +import { ModelDetailsButton } from '@/features/start-mapping/components/model-details-button'; +import { ModelPredictionsTracker } from '@/features/start-mapping/components/model-predictions-tracker'; +import { ModelSettings } from '@/features/start-mapping/components/model-settings'; +import { SkeletonWrapper } from '@/components/ui/skeleton'; +import { TDownloadOptions, TQueryParams } from '@/app/routes/start-mapping'; +import { TModel, TModelPredictions, TModelPredictionsConfig } from '@/types'; +import { ToolTip } from '@/components/ui/tooltip'; +import { useDropdownMenu } from '@/hooks/use-dropdown-menu'; +import { UserProfile } from '@/components/layout'; import { - ELEMENT_DISTANCE_FROM_NAVBAR, + START_MAPPING_PAGE_CONTENT, } from "@/constants"; + const StartMappingHeader = ({ data, modelPredictions, @@ -109,8 +111,8 @@ const StartMappingHeader = ({ content={ !modelPredictionsExist ? START_MAPPING_PAGE_CONTENT.actions.disabledModeTooltip( - "see actions", - ) + "see actions", + ) : null } > diff --git a/frontend/src/features/start-mapping/components/logo-with-dropdown.tsx b/frontend/src/features/start-mapping/components/logo-with-dropdown.tsx index 8010f3eb..463497b6 100644 --- a/frontend/src/features/start-mapping/components/logo-with-dropdown.tsx +++ b/frontend/src/features/start-mapping/components/logo-with-dropdown.tsx @@ -1,7 +1,7 @@ import { Divider } from '@/components/ui/divider'; import { DropDown } from '@/components/ui/dropdown'; import { DropdownPlacement } from '@/enums'; -import { ELEMENT_DISTANCE_FROM_NAVBAR } from '@/constants'; +import { ELEMENT_DISTANCE_FROM_NAVBAR } from '@/config'; import { Link } from '@/components/ui/link'; import { navLinks } from '@/constants/general'; import { NavLogo } from '@/components/layout'; diff --git a/frontend/src/features/start-mapping/components/map/legend-control.tsx b/frontend/src/features/start-mapping/components/map/legend-control.tsx index aa490d2f..341a2286 100644 --- a/frontend/src/features/start-mapping/components/map/legend-control.tsx +++ b/frontend/src/features/start-mapping/components/map/legend-control.tsx @@ -1,12 +1,15 @@ -import useScreenSize from "@/hooks/use-screen-size"; -import { LegendBookIcon } from "@/components/ui/icons"; -import { Map } from "maplibre-gl"; -import { useCallback, useState } from "react"; +import useScreenSize from '@/hooks/use-screen-size'; +import { LegendBookIcon } from '@/components/ui/icons'; +import { Map } from 'maplibre-gl'; +import { START_MAPPING_PAGE_CONTENT } from '@/constants'; +import { useCallback, useState } from 'react'; import { - START_MAPPING_PAGE_CONTENT, + LEGEND_NAME_MAPPING, MAP_STYLES_PREFIX, -} from "@/constants"; +} from "@/config"; + + const FillLegendStyle = ({ fillColor, diff --git a/frontend/src/features/start-mapping/components/map/map.tsx b/frontend/src/features/start-mapping/components/map/map.tsx index 115247db..2402d4c1 100644 --- a/frontend/src/features/start-mapping/components/map/map.tsx +++ b/frontend/src/features/start-mapping/components/map/map.tsx @@ -5,6 +5,7 @@ import { extractTileJSONURL, showErrorToast } from '@/utils'; import { GeoJSONSource, LngLatBoundsLike, Map } from 'maplibre-gl'; import { Legend } from '@/features/start-mapping/components'; import { MapComponent, MapCursorToolTip } from '@/components/map'; +import { TOAST_NOTIFICATIONS } from '@/constants'; import { useMapLayers } from '@/hooks/use-map-layer'; import { useToolTipVisibility } from '@/hooks/use-tooltip-visibility'; import { @@ -24,7 +25,7 @@ import { TTrainingDataset, } from "@/types"; import { - TOAST_NOTIFICATIONS, + ACCEPTED_MODEL_PREDICTIONS_FILL_LAYER_ID, ACCEPTED_MODEL_PREDICTIONS_OUTLINE_LAYER_ID, ACCEPTED_MODEL_PREDICTIONS_SOURCE_ID, @@ -36,7 +37,8 @@ import { REJECTED_MODEL_PREDICTIONS_FILL_LAYER_ID, REJECTED_MODEL_PREDICTIONS_OUTLINE_LAYER_ID, REJECTED_MODEL_PREDICTIONS_SOURCE_ID, -} from "@/constants"; +} from "@/config"; + export const StartMappingMapComponent = ({ trainingDataset, diff --git a/frontend/src/features/start-mapping/components/mobile-drawer.tsx b/frontend/src/features/start-mapping/components/mobile-drawer.tsx index 292cd5ba..a7491be4 100644 --- a/frontend/src/features/start-mapping/components/mobile-drawer.tsx +++ b/frontend/src/features/start-mapping/components/mobile-drawer.tsx @@ -1,17 +1,17 @@ -import ModelAction from "@/features/start-mapping/components/model-action"; -import { ChevronDownIcon, CloudDownloadIcon } from "@/components/ui/icons"; -import { Map } from "maplibre-gl"; -import { MobileDrawer } from "@/components/ui/drawer"; -import { ModelDetailsButton } from "@/features/start-mapping/components/model-details-button"; -import { ModelPredictionsTracker } from "@/features/start-mapping/components/model-predictions-tracker"; -import { ModelSettings } from "@/features/start-mapping/components/model-settings"; -import { TDownloadOptions, TQueryParams } from "@/app/routes/start-mapping"; -import { TModelPredictions, TModelPredictionsConfig } from "@/types"; -import { ToolTip } from "@/components/ui/tooltip"; -import { useState } from "react"; +import ModelAction from '@/features/start-mapping/components/model-action'; +import { ChevronDownIcon, CloudDownloadIcon } from '@/components/ui/icons'; +import { Map } from 'maplibre-gl'; +import { MINIMUM_ZOOM_LEVEL_INSTRUCTION_FOR_PREDICTION } from '@/config'; +import { MobileDrawer } from '@/components/ui/drawer'; +import { ModelDetailsButton } from '@/features/start-mapping/components/model-details-button'; +import { ModelPredictionsTracker } from '@/features/start-mapping/components/model-predictions-tracker'; +import { ModelSettings } from '@/features/start-mapping/components/model-settings'; +import { TDownloadOptions, TQueryParams } from '@/app/routes/start-mapping'; +import { TModelPredictions, TModelPredictionsConfig } from '@/types'; +import { ToolTip } from '@/components/ui/tooltip'; +import { useState } from 'react'; import { START_MAPPING_PAGE_CONTENT, - MINIMUM_ZOOM_LEVEL_INSTRUCTION_FOR_PREDICTION, } from "@/constants"; export const StartMappingMobileDrawer = ({ @@ -87,8 +87,8 @@ export const StartMappingMobileDrawer = ({ content={ disablePrediction ? START_MAPPING_PAGE_CONTENT.actions.disabledModeTooltip( - "see download options", - ) + "see download options", + ) : null } > diff --git a/frontend/src/features/start-mapping/components/model-details-popup.tsx b/frontend/src/features/start-mapping/components/model-details-popup.tsx index dae40aa9..24b1b972 100644 --- a/frontend/src/features/start-mapping/components/model-details-popup.tsx +++ b/frontend/src/features/start-mapping/components/model-details-popup.tsx @@ -1,4 +1,5 @@ import useScreenSize from '@/hooks/use-screen-size'; +import { ELEMENT_DISTANCE_FROM_NAVBAR } from '@/config'; import { extractDatePart, roundNumber, truncateString } from '@/utils'; import { MobileDrawer } from '@/components/ui/drawer'; import { Popup } from '@/components/ui/popup'; @@ -7,7 +8,6 @@ import { TModelDetails, TTrainingDataset } from '@/types'; import { useModelDetails } from '@/features/models/hooks/use-models'; import { useTrainingDetails } from '@/features/models/hooks/use-training'; import { - ELEMENT_DISTANCE_FROM_NAVBAR, START_MAPPING_PAGE_CONTENT, } from "@/constants"; diff --git a/frontend/src/features/start-mapping/components/model-settings.tsx b/frontend/src/features/start-mapping/components/model-settings.tsx index 5559faa4..42cba2ad 100644 --- a/frontend/src/features/start-mapping/components/model-settings.tsx +++ b/frontend/src/features/start-mapping/components/model-settings.tsx @@ -1,18 +1,20 @@ import { DropDown } from '@/components/ui/dropdown'; +import { ELEMENT_DISTANCE_FROM_NAVBAR } from '@/config'; import { FormLabel, Input, Select, Switch -} from '@/components/ui/form'; + } from '@/components/ui/form'; import { SEARCH_PARAMS, TQueryParams } from '@/app/routes/start-mapping'; import { SettingsIcon } from '@/components/ui/icons'; import { ToolTip } from '@/components/ui/tooltip'; import { useDropdownMenu } from '@/hooks/use-dropdown-menu'; import { - ELEMENT_DISTANCE_FROM_NAVBAR, + START_MAPPING_PAGE_CONTENT, } from "@/constants"; + import { DropdownPlacement, INPUT_TYPES, diff --git a/frontend/src/hooks/use-layer-order.ts b/frontend/src/hooks/use-layer-order.ts index ce8f41cf..141131e0 100644 --- a/frontend/src/hooks/use-layer-order.ts +++ b/frontend/src/hooks/use-layer-order.ts @@ -1,6 +1,6 @@ -import { Map } from "maplibre-gl"; -import { TILE_BOUNDARY_LAYER_ID, TMS_LAYER_ID } from "@/constants"; -import { useEffect, useRef } from "react"; +import { Map } from 'maplibre-gl'; +import { TILE_BOUNDARY_LAYER_ID, TMS_LAYER_ID } from '@/config'; +import { useEffect, useRef } from 'react'; type UseLayerReorderProps = { /** IDs of all feature layers. (We want them above TMS.) */ diff --git a/frontend/src/hooks/use-login.ts b/frontend/src/hooks/use-login.ts index 88a8a799..14b5960d 100644 --- a/frontend/src/hooks/use-login.ts +++ b/frontend/src/hooks/use-login.ts @@ -1,12 +1,13 @@ import { authService } from '@/services'; +import { HOT_FAIR_SESSION_REDIRECT_KEY } from '@/config'; import { showErrorToast } from '@/utils'; import { useLocation } from 'react-router-dom'; import { useSessionStorage } from '@/hooks/use-storage'; import { useState } from 'react'; import { TOAST_NOTIFICATIONS, - HOT_FAIR_SESSION_REDIRECT_KEY, } from "@/constants"; + /** * Custom hook to handle the login button click event. * diff --git a/frontend/src/services/api-client.ts b/frontend/src/services/api-client.ts index 0e2493c4..4eaf32c3 100644 --- a/frontend/src/services/api-client.ts +++ b/frontend/src/services/api-client.ts @@ -1,8 +1,7 @@ -import Axios, { InternalAxiosRequestConfig } from "axios"; -import { ENVS } from "@/config/env"; -import { HOT_FAIR_LOCAL_STORAGE_ACCESS_TOKEN_KEY } from "@/constants"; -import { showErrorToast } from "@/utils"; -export const BASE_API_URL = ENVS.BASE_API_URL; +import Axios, { InternalAxiosRequestConfig } from 'axios'; +import { BASE_API_URL, HOT_FAIR_LOCAL_STORAGE_ACCESS_TOKEN_KEY } from '@/config'; +import { showErrorToast } from '@/utils'; + /** * The global axios API client. */ diff --git a/frontend/src/services/api-routes.ts b/frontend/src/services/api-routes.ts index 92efbaa9..0630952f 100644 --- a/frontend/src/services/api-routes.ts +++ b/frontend/src/services/api-routes.ts @@ -1,4 +1,4 @@ -import { ENVS } from '@/config/env'; +import { FAIR_PREDICTOR_API_ENDPOINT, OSM_DATABASE_STATUS_API_ENDPOINT } from '@/config'; /** * The backend API endpoints. @@ -12,11 +12,11 @@ export const API_ENDPOINTS = { // OSM Database - GET_OSM_DATABASE_LAST_UPDATED: ENVS.OSM_DATABASE_STATUS_API_URL || "https://api-prod.raw-data.hotosm.org/v1/status/", + GET_OSM_DATABASE_LAST_UPDATED: OSM_DATABASE_STATUS_API_ENDPOINT, // Predict - GET_MODEL_PREDICTIONS: ENVS.FAIR_PREDICTOR_API_URL || "https://predictor-dev.fair.hotosm.org/predict/", + GET_MODEL_PREDICTIONS: FAIR_PREDICTOR_API_ENDPOINT, // Feedbacks diff --git a/frontend/src/utils/geo/geo-utils.ts b/frontend/src/utils/geo/geo-utils.ts index df9cdb13..0e9bcc49 100644 --- a/frontend/src/utils/geo/geo-utils.ts +++ b/frontend/src/utils/geo/geo-utils.ts @@ -1,16 +1,18 @@ import bbox from '@turf/bbox'; -import { API_ENDPOINTS, BASE_API_URL } from '@/services'; -import { calculateGeoJSONArea } from './geometry-utils'; -import { Feature, FeatureCollection } from 'geojson'; -import { geojsonToOsmPolygons } from './geojson-to-osm'; -import { showErrorToast, showSuccessToast } from '../general-utils'; import { + BASE_API_URL, JOSM_REMOTE_URL, MAX_TRAINING_AREA_SIZE, MIN_TRAINING_AREA_SIZE, - OSM_HASHTAGS, - TOAST_NOTIFICATIONS, -} from "@/constants"; + OSM_HASHTAGS + } from '@/config'; +import { calculateGeoJSONArea } from './geometry-utils'; +import { Feature, FeatureCollection } from 'geojson'; +import { geojsonToOsmPolygons } from './geojson-to-osm'; +import { showErrorToast, showSuccessToast } from '../general-utils'; +import { TOAST_NOTIFICATIONS } from '@/constants'; +import { API_ENDPOINTS, } from '@/services'; + /** * Creates a GeoJSON FeatureCollection diff --git a/frontend/src/utils/geo/geometry-utils.ts b/frontend/src/utils/geo/geometry-utils.ts index f5d9018a..fa355fd8 100644 --- a/frontend/src/utils/geo/geometry-utils.ts +++ b/frontend/src/utils/geo/geometry-utils.ts @@ -1,12 +1,17 @@ -import area from "@turf/area"; -import bboxPolygon from "@turf/bbox"; -import { booleanIntersects } from "@turf/boolean-intersects"; -import { createFeatureCollection } from "./geo-utils"; -import { Feature, FeatureCollection, Polygon, Position } from "geojson"; -import { LngLatBoundsLike, Map } from "maplibre-gl"; -import { roundNumber } from "../number-utils"; -import { TModelPredictions, TModelPredictionsConfig } from "@/types"; -import { uuid4 } from "../general-utils"; +import area from '@turf/area'; +import bboxPolygon from '@turf/bbox'; +import { booleanIntersects } from '@turf/boolean-intersects'; +import { createFeatureCollection } from './geo-utils'; +import { + Feature, + FeatureCollection, + Polygon, + Position + } from 'geojson'; +import { LngLatBoundsLike, Map } from 'maplibre-gl'; +import { roundNumber } from '../number-utils'; +import { TModelPredictions, TModelPredictionsConfig } from '@/types'; +import { uuid4 } from '../general-utils'; /** * Calculates the area of a GeoJSON Feature or FeatureCollection. @@ -36,12 +41,14 @@ export const calculateGeoJSONArea = ( * @returns {string} The result as 12,222,000 m² or 12,222 km² */ -export const formatAreaInAppropriateUnit = (area: number): string => { - if (area > 1000000) { - return roundNumber(area / 1000000, 1).toLocaleString() + "km²"; +export function formatAreaInAppropriateUnit(area: number) { + const SQUARE_METERS_IN_SQUARE_KILOMETER = 1000000; + if (area > SQUARE_METERS_IN_SQUARE_KILOMETER) { + return roundNumber(area / SQUARE_METERS_IN_SQUARE_KILOMETER, 1).toLocaleString() + "km²"; } return roundNumber(area, 1).toLocaleString() + "m²"; }; + /** * Computes the bounding box of a GeoJSON Feature. * diff --git a/frontend/src/utils/string-utils.ts b/frontend/src/utils/string-utils.ts index 0e6f11a7..18bcdf8c 100644 --- a/frontend/src/utils/string-utils.ts +++ b/frontend/src/utils/string-utils.ts @@ -1,4 +1,4 @@ -import { OAM_S3_BUCKET_URL, OAM_TITILER_ENDPOINT } from '@/constants'; +import { OAM_S3_BUCKET_URL, OAM_TITILER_ENDPOINT } from '@/config'; /** * Truncates a string to a specified maximum length, appending ellipsis if truncated. From 49d6710e95cf92dc4f45a499c52708272b7a1c2b Mon Sep 17 00:00:00 2001 From: jeafreezy Date: Sat, 22 Feb 2025 15:19:22 +0100 Subject: [PATCH 04/12] chore: updated training area config comment with default --- frontend/src/config/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/config/index.ts b/frontend/src/config/index.ts index 5dae6282..960c3f98 100644 --- a/frontend/src/config/index.ts +++ b/frontend/src/config/index.ts @@ -119,6 +119,7 @@ export const MAX_TRAINING_AREA_SIZE: number = parseIntEnv(ENVS.MAX_TRAINING_AREA /** * The minimum allowed area size (in square meters) for training areas. + * The default is set to 5797 sq. meters (1.43 acres). */ export const MIN_TRAINING_AREA_SIZE: number = parseIntEnv(ENVS.MIN_TRAINING_AREA_SIZE, 5797); From 889d87a5ddf3729b5ea51626ed7b6af22f14efbe Mon Sep 17 00:00:00 2001 From: jeafreezy Date: Mon, 24 Feb 2025 13:30:35 +0100 Subject: [PATCH 05/12] chore: progress with auth flow fix --- frontend/src/app/router.tsx | 11 ++++ frontend/src/app/routes/login.tsx | 47 ++++++++++++++++++ frontend/src/app/routes/protected-route.tsx | 13 ++--- frontend/src/assets/images/index.ts | 1 + frontend/src/assets/images/osm_logo.png | Bin 0 -> 2931 bytes frontend/src/components/auth/index.ts | 1 + .../src/components/auth/osm-login-modal.tsx | 47 ++++++++++++++++++ .../src/components/layout/footer/footer.tsx | 22 +------- .../src/components/layout/navbar/navbar.tsx | 26 +++++----- frontend/src/components/shared/index.ts | 1 + .../src/components/shared/made-with-love.tsx | 27 ++++++++++ frontend/src/constants/routes.ts | 2 + frontend/src/hooks/use-login.ts | 4 +- frontend/src/layouts/root-layout.tsx | 20 ++++++-- 14 files changed, 175 insertions(+), 47 deletions(-) create mode 100644 frontend/src/app/routes/login.tsx create mode 100644 frontend/src/assets/images/osm_logo.png create mode 100644 frontend/src/components/auth/index.ts create mode 100644 frontend/src/components/auth/osm-login-modal.tsx create mode 100644 frontend/src/components/shared/made-with-love.tsx diff --git a/frontend/src/app/router.tsx b/frontend/src/app/router.tsx index 40e283ff..c13942cf 100644 --- a/frontend/src/app/router.tsx +++ b/frontend/src/app/router.tsx @@ -321,6 +321,17 @@ const router = createBrowserRouter([ * User account routes ends */ + /** + * Auth route + */ + { + path: APPLICATION_ROUTES.LOGIN, + lazy: async () => { + const { LoginPage } = await import("@/app/routes/login"); + return { Component: LoginPage }; + }, + }, + /** * 404 route */ diff --git a/frontend/src/app/routes/login.tsx b/frontend/src/app/routes/login.tsx new file mode 100644 index 00000000..d55ff09c --- /dev/null +++ b/frontend/src/app/routes/login.tsx @@ -0,0 +1,47 @@ +import { OSMLogo, } from "@/assets/images"; + +import { NavLogo } from "@/components/layout" +import { MadeWithLove } from "@/components/shared"; +import { Dialog } from "@/components/ui/dialog"; +import { Image } from "@/components/ui/image"; +import { useDialog } from "@/hooks/use-dialog"; +import { useLogin } from "@/hooks/use-login"; +import { useLocation, useNavigate } from "react-router-dom"; + + + +export const LoginPage = () => { + + const { closeDialog } = useDialog(); + const navigate = useNavigate(); + const { handleLogin } = useLogin(); + const location = useLocation(); + + console.log(location.state); + + const handleOnClose = () => { + closeDialog(); + navigate(-1); + }; + + return ( + +
+ +

Welcome to fAIr

+ + +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/app/routes/protected-route.tsx b/frontend/src/app/routes/protected-route.tsx index d75ef27f..4c8c62d1 100644 --- a/frontend/src/app/routes/protected-route.tsx +++ b/frontend/src/app/routes/protected-route.tsx @@ -3,7 +3,7 @@ import { Head } from "@/components/seo"; import { SHARED_CONTENT } from "@/constants"; import { ShieldIcon } from "@/components/ui/icons"; import { useAuth } from "@/app/providers/auth-provider"; -import { useLogin } from "@/hooks/use-login"; +import { useLocation, useNavigate } from "react-router-dom"; type ProtectedRouteProps = { children: React.ReactNode; @@ -11,8 +11,8 @@ type ProtectedRouteProps = { export const ProtectedRoute: React.FC = ({ children }) => { const { isAuthenticated } = useAuth(); - const { handleLogin, loading } = useLogin(); - + const navigate = useNavigate(); + const location = useLocation() if (!isAuthenticated) { return ( <> @@ -33,13 +33,10 @@ export const ProtectedRoute: React.FC = ({ children }) => {
diff --git a/frontend/src/assets/images/index.ts b/frontend/src/assets/images/index.ts index c535dadd..f726a03e 100644 --- a/frontend/src/assets/images/index.ts +++ b/frontend/src/assets/images/index.ts @@ -9,3 +9,4 @@ export { default as ModelFormConfirmation } from "@/assets/images/model_creation export { default as FairModelPlaceholderImage } from "@/assets/images/model_placeholder_image.png"; export { default as TrainingInProgressImage } from "@/assets/images/training_in_progress.png"; export { default as fAIrLogo } from "@/assets/images/fAIr_logo.png"; +export { default as OSMLogo } from "@/assets/images/osm_logo.png"; diff --git a/frontend/src/assets/images/osm_logo.png b/frontend/src/assets/images/osm_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..e11ed924b0a2bc634ac5e90f1fcf71fa35a5e3f5 GIT binary patch literal 2931 zcmV-(3yk!MP)K~#7F#aDTd z6!jJU`k3yXo}P1$*@Ip7UcluLxx=v{1t^#jr4ldVfyR_cG>Ha7B9SViQH&-rUSO4? zrYai6BM<`^4naXK5#-)oVDH_Tonv>d?w+3Rycu@MLj2dtZ>ndeXMWx9z3+YRd#^dr zTJ?u}C(plWR#nBAa(l+XtGz9~Y5JKNJcicr?^wC+$BX^tiY=YbYvJawHuyhFhc#^f z&^62lyVl-NQ(d0ez5PH8m)(?k@bSCr9{ct3QxSatil{(~YoV>X_x!12(Qltz4Vc-D zi*8-yojzsE-0K$1yU$zen%fx(u&s#`jN(*aXGAU4q`vdoE1Mqu==H7b+5JCTeS@d4 zux@taoGA}qHD>zEo@6@)0`UW5E9XtcH$~MP-F!Zhj+T-CtYyJs=_nW7Y&0#L9N`;L zV=;^^o{phA(TDE-03=>QYoZBGUdB};Mx(v^oOSwiAJcY5Z>p-OEe2yM!QNKXG~}V; z%1YQGHq;i4hRBTIFc?&Jaf6?%Jx@A2Y08*ebi;sUk-3&Q6DnY!rIQ6JbS|Eb<6!Rz zq|^jNyM&C9LEK1ScKK*L@{8Bu_g>2^z2;`joH`?~wz68NuByh=DOV#kP=FWLyo=f@ z8Abj=wDfemb^Oq&$p0YF*w`p-*!D!KIoJlsJlH(}7_*9CMw9R;1-U(zY2j4xJQTZv zjAkHdMX>oVA-wR%$I#Z(3fMA&vhd1Ac{f9fzd3qy?%lr@Ae%*sF z3~_}WLSX04ooX-^2_cJr%79ragWc5yGmt`LAc$l#fpl6!y|)Z%JPyeX?0hAG=bm1H zU^EDq;sh^q@VecQ9d>Xs1JNnMWKHyT_0oTRm^}L)>s(XovSCEM=s*?;+wNTlH)a2^ zGA2x)fW(z92q6Q!N1|Ud(9@KRjEZU14S4C1ZFpwQ!{{35fzE2^H)3Ec4t=Qz2J|@X zHsBB?C@u*uuN_)SMJRESsh_ZPGTlrVvOVNLCi}+2Ip9-Dph$V(Oc7q*4a+S;>q~$Y zC?Y#u$5`0CZx3$1;{h~xHxpY;xCAG>k_Q~mV{HCtctj`Mf(ONpQi_H}SE>^gBa3p# zPQ0=zGkgA`bwemzQriiuInx$xWobcERoLxz=$Z~82?Y3FsEHH?Y$xgxV`XSI7U_%)Vy2#q8OWIZ zL_8KEIgmE|(1|-Xr4v*%knJL(_5>QL#${9Br6V$w0?b)3<<_9u3)3=kM;04{O47_2 zNJj<`MuH%=h=44-o;+eXMU%0RN)um9Gv{!zM?f*nG%TN@IiL^!KN%y5H2G4U~NPQTF$qXkUN=pjrv+!r5 zx9C%6(y;C8L;5@UNXCfOOcK@3Y6!0LbRGpu3YaEbO)g0#*8|aYjts@n(@*?2vc}0L zvZy!`&}bh8PwAoGSq@gxL@XBTzKB3gO^p~f0#xg9+Q37WCDQu2U!~ zsYXgq!X`$Mp%5uR@HQO|B|;S9;PRa%$>wvVJA#663N9+mVr%)`cfD9WZsx&`mJ{kw z3goxeee}9ww=>x^xP2}pzvw`^!3j?F-*OZDej9XEqI6K1 zS8y+#{(fB=|&+@yu)M@%5oY52X{`o-73}D&gjj_H8L}`f^&H zErG0_bB?U=WSf(UV8jh?;%XSZGrCZQo(Mo4Vw1v6$83eUA*`{8y-T>-3n z{(0&b66t>u*WW%Ho7TSy##Tv9$dA1IBFN;7A`~Feoxyvb?!p^y`~$cB@M?quM<5Ek zu1lw$9WGbpc;&F{=D zo$$`u&57=QK5N^&q!+Yw?hYgcHGd%LXJCuLuCzmtc?b;Z4 z{?MV_K{9DvM4+y*F2At2vfy}IHw5o-n)5uc5&L&+J&fm8yo<_;LKq~!;dl&g7cgo@ zEnLU?k*U*BqDd&8H5qDO9FhJoLarcK5+o&Gh9L431dA4{4-3ZvcV2U3OP{1hv&Jt` z!tc#rc2mbdA9aTl2qttmWe2u>d>CsU`Wwnd_{g}1Kr}*qUBl9!UV{dzj?65&@jx9k z(op>~nxp~=6r}8!B-W#{p$syKmEsY>lPuE6fNjS&$NT!452B+ZHDvc90uA##cb@P6 zn#9753U3iUe18Yltb7wyBmFt!;TR1ZG^+jJwz-%#=L*QwsN&HCjax+4&I%MxYlD%b z4l8lghD97dcMK#CqzR;^HtLolgQL5f_K_)dc-k)_P%*N66lby+Nu~4N+uQN`6|bSZ zG>_y|M<5afBXU^&@M6roq7njepjW6Mu@T|)H9?faXgWHH^npfxYDArV?Ya3d7a?(t zA(eEv?Kt%BV;^GJL_FL9z96r_BhmP@{>@F+Q!8IZS;+ zu?kBjqRt!DYRhDb%s~UoH;(7CMOL}n}{S0FE&xkH6f1rO0~ZX;jZw(%N@w+ zfV0Huvpc1$zSw%;#NmG(t>;MvEov@V*sDj6#EK_x!<@$PBqXMJX#4Rq58m_m)2mjj zePZp48$Jp3hV=2{>Pu?MYGj)fqsF6?Q4OAaHynj_q*F%f^Nn8~SiSt^-!*;J_BokL z4tHRv471}AwJe{QjQ;h@IgO9{La`W2qjgVFk*|I1)GBM+#xMTa)z-C@_Vv(_WpVZq zYiP2uOOjN%@WzRYzkBN)i;sQz>DhhTPV78*@ZXw>g(a{%s+AON+VeoSiVC(dBS3etJ4H;&Hy`=+UhS{2$ik dkLKUn`VU<}+aoh@l|ldj002ovPDHLkV1gN3h<^Y8 literal 0 HcmV?d00001 diff --git a/frontend/src/components/auth/index.ts b/frontend/src/components/auth/index.ts new file mode 100644 index 00000000..042530e5 --- /dev/null +++ b/frontend/src/components/auth/index.ts @@ -0,0 +1 @@ +export { OpenStreetMapAuthModal } from './osm-login-modal' \ No newline at end of file diff --git a/frontend/src/components/auth/osm-login-modal.tsx b/frontend/src/components/auth/osm-login-modal.tsx new file mode 100644 index 00000000..db62e0ce --- /dev/null +++ b/frontend/src/components/auth/osm-login-modal.tsx @@ -0,0 +1,47 @@ +import { OSMLogo, } from "@/assets/images"; + +import { NavLogo } from "@/components/layout" +import { MadeWithLove } from "@/components/shared"; +import { Dialog } from "@/components/ui/dialog"; +import { Image } from "@/components/ui/image"; +import { useDialog } from "@/hooks/use-dialog"; +import { useLogin } from "@/hooks/use-login"; +import { useLocation, useNavigate } from "react-router-dom"; + + + +export const OpenStreetMapAuthModal = () => { + + const { closeDialog } = useDialog(); + const navigate = useNavigate(); + const { handleLogin } = useLogin(); + const location = useLocation(); + + console.log(location.state); + + const handleOnClose = () => { + closeDialog(); + navigate(-1); + }; + + return ( + +
+ +

Welcome to fAIr

+ + +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/layout/footer/footer.tsx b/frontend/src/components/layout/footer/footer.tsx index 26db9e1c..fe0a74dd 100644 --- a/frontend/src/components/layout/footer/footer.tsx +++ b/frontend/src/components/layout/footer/footer.tsx @@ -9,6 +9,7 @@ import { XIcon, YouTubeIcon, } from "@/assets/svgs"; +import { MadeWithLove } from "@/components/shared"; const socials = [ { @@ -122,26 +123,7 @@ export const Footer = () => {
-

- {SHARED_CONTENT.footer.madeWithLove.firstSegment} - - {SHARED_CONTENT.footer.madeWithLove.secondSegment} - - {SHARED_CONTENT.footer.madeWithLove.thirdSegment} - - {SHARED_CONTENT.footer.madeWithLove.fourthSegment} - -

+
); diff --git a/frontend/src/components/layout/navbar/navbar.tsx b/frontend/src/components/layout/navbar/navbar.tsx index 7f06dc94..a72c6718 100644 --- a/frontend/src/components/layout/navbar/navbar.tsx +++ b/frontend/src/components/layout/navbar/navbar.tsx @@ -9,15 +9,16 @@ import { navLinks } from "@/constants/general"; import { NavLogo } from "@/components/layout"; import { SHARED_CONTENT } from "@/constants"; import { useAuth } from "@/app/providers/auth-provider"; -import { useLocation } from "react-router-dom"; -import { useLogin } from "@/hooks/use-login"; +import { useLocation, useNavigate } from "react-router-dom"; + import { UserProfile } from "@/components/layout"; import { useState } from "react"; export const NavBar = () => { const [open, setOpen] = useState(false); const { isAuthenticated } = useAuth(); - const { handleLogin, loading } = useLogin(); + const navigate = useNavigate(); + const location = useLocation() return ( <> @@ -39,10 +40,10 @@ export const NavBar = () => { {isAuthenticated ? ( ) : ( - )} @@ -62,12 +63,11 @@ export const NavBar = () => { )} @@ -80,7 +80,7 @@ export const NavBar = () => { height="20px" /> - + ); }; diff --git a/frontend/src/components/shared/index.ts b/frontend/src/components/shared/index.ts index e4a50853..5d1478fb 100644 --- a/frontend/src/components/shared/index.ts +++ b/frontend/src/components/shared/index.ts @@ -4,3 +4,4 @@ export { SectionHeader } from "./section-header"; export * from "./pagination"; export { TheFAIRProcess } from "./fair-process/fair-process"; export { HotTracking } from "./hot-tracking"; +export { MadeWithLove } from "./made-with-love"; \ No newline at end of file diff --git a/frontend/src/components/shared/made-with-love.tsx b/frontend/src/components/shared/made-with-love.tsx new file mode 100644 index 00000000..1e4d1498 --- /dev/null +++ b/frontend/src/components/shared/made-with-love.tsx @@ -0,0 +1,27 @@ +import { SHARED_CONTENT } from "@/constants" +import { Link } from "@/components/ui/link" + +export const MadeWithLove = () => { + return ( +

+ {SHARED_CONTENT.footer.madeWithLove.firstSegment} + + {SHARED_CONTENT.footer.madeWithLove.secondSegment} + + {SHARED_CONTENT.footer.madeWithLove.thirdSegment} + + {SHARED_CONTENT.footer.madeWithLove.fourthSegment} + +

+ ) +} \ No newline at end of file diff --git a/frontend/src/constants/routes.ts b/frontend/src/constants/routes.ts index 514aadd8..b3fb71bd 100644 --- a/frontend/src/constants/routes.ts +++ b/frontend/src/constants/routes.ts @@ -44,6 +44,8 @@ export const APPLICATION_ROUTES = { START_MAPPING_BASE: "/start-mapping/", START_MAPPING: "/start-mapping/:modelId", NOTFOUND: "/404", + LOGIN: "/login", + LOGIN: "/404", PRIVACY_POLICY: "/privacy", LEARN: "/learn", ABOUT: "/about", diff --git a/frontend/src/hooks/use-login.ts b/frontend/src/hooks/use-login.ts index 14b5960d..1bd495f3 100644 --- a/frontend/src/hooks/use-login.ts +++ b/frontend/src/hooks/use-login.ts @@ -23,13 +23,13 @@ import { */ export const useLogin = () => { const location = useLocation(); - const currentPath = location.pathname; + const { setSessionValue } = useSessionStorage(); const [loading, setLoading] = useState(false); const handleLogin = async (): Promise => { setLoading(true); - setSessionValue(HOT_FAIR_SESSION_REDIRECT_KEY, currentPath); + setSessionValue(HOT_FAIR_SESSION_REDIRECT_KEY, location.pathname); try { await authService.initializeOAuthFlow(); } catch (error) { diff --git a/frontend/src/layouts/root-layout.tsx b/frontend/src/layouts/root-layout.tsx index 16484995..538d1eae 100644 --- a/frontend/src/layouts/root-layout.tsx +++ b/frontend/src/layouts/root-layout.tsx @@ -6,31 +6,43 @@ import { NavBar } from "@/components/layout"; import { Outlet, useLocation } from "react-router-dom"; import { useEffect } from "react"; import { useScrollToTop } from "@/hooks/use-scroll-to-element"; +import { OpenStreetMapAuthModal } from "@/components/auth"; export const RootLayout = () => { - const { pathname } = useLocation(); + const { pathname, state } = useLocation(); const { scrollToTop } = useScrollToTop(); // Scroll to top on pages switch. useEffect(() => { scrollToTop(); }, [pathname]); + const pagesWithoutNavbarAndFooter = [ + APPLICATION_ROUTES.OSM_LOGIN, + APPLICATION_ROUTES.START_MAPPING_BASE + ]; + + const pagesWithoutPadding = [ + APPLICATION_ROUTES.HOMEPAGE + ] return ( <> + { + state?.backgroundLocation && + }
- {!pathname.includes(APPLICATION_ROUTES.START_MAPPING_BASE) && ( + {!pagesWithoutNavbarAndFooter.includes(pathname) && ( )}
- {!pathname.includes(APPLICATION_ROUTES.START_MAPPING_BASE) && ( + {!pagesWithoutNavbarAndFooter.includes(pathname) && (
)}
From 4fa2337334f9a4dc18923408f3b2a12395538c7b Mon Sep 17 00:00:00 2001 From: jeafreezy Date: Mon, 24 Feb 2025 19:00:58 +0100 Subject: [PATCH 06/12] feat: enhanced auth flow + tests --- frontend/package.json | 2 + frontend/pnpm-lock.yaml | 369 +++++++++++++++++- frontend/src/app/providers/auth-provider.tsx | 36 +- .../src/app/providers/models-provider.tsx | 49 +-- frontend/src/app/router.tsx | 16 +- frontend/src/app/routes/authenticate.tsx | 32 ++ frontend/src/app/routes/learn.tsx | 2 +- frontend/src/app/routes/login.tsx | 47 --- .../app/routes/models/model-details-form.tsx | 2 +- .../app/routes/models/training-dataset.tsx | 2 +- frontend/src/app/routes/protected-route.tsx | 6 +- frontend/src/app/routes/resources.tsx | 3 +- frontend/src/app/routes/start-mapping.tsx | 95 ++--- frontend/src/components/auth/auth-modal.tsx | 62 +++ frontend/src/components/auth/index.ts | 2 +- .../src/components/auth/osm-login-modal.tsx | 47 --- frontend/src/components/landing/kpi/kpi.tsx | 12 +- .../src/components/layout/navbar/navbar.tsx | 23 +- .../components/layout/navbar/user-profile.tsx | 29 +- .../components/map/controls/layer-control.tsx | 28 +- .../src/components/map/layers/basemaps.tsx | 4 +- .../components/map/layers/open-aerial-map.tsx | 6 +- .../components/map/layers/tile-boundaries.tsx | 12 +- .../components/map/setups/setup-maplibre.ts | 8 +- .../components/map/setups/setup-terra-draw.ts | 4 +- .../src/components/shared/hot-tracking.tsx | 14 +- frontend/src/components/shared/index.ts | 2 +- .../src/components/shared/made-with-love.tsx | 50 +-- frontend/src/components/ui/dialog/dialog.css | 4 + frontend/src/components/ui/dialog/dialog.tsx | 18 +- .../ui/form/form-label/form-label.tsx | 6 +- .../ui/form/help-text/help-text.tsx | 14 +- .../src/components/ui/form/input/input.tsx | 30 +- .../src/components/ui/form/select/select.tsx | 12 +- .../ui/form/text-area/text-area.tsx | 6 +- frontend/src/config/__tests__/config.test.ts | 93 ++--- frontend/src/config/env.ts | 51 ++- frontend/src/config/index.ts | 251 +++++++----- frontend/src/constants/routes.ts | 3 +- .../src/constants/ui-contents/auth-content.ts | 10 + .../src/constants/ui-contents/map-content.ts | 2 +- .../constants/ui-contents/models-content.ts | 64 +-- .../constants/ui-contents/shared-content.ts | 4 +- .../ui-contents/start-mapping-content.ts | 2 +- .../components/dialogs/file-upload-dialog.tsx | 29 +- .../model-details/model-description-input.tsx | 4 +- .../model-details/model-details.tsx | 19 +- .../model-details/model-name-input.tsx | 4 +- .../components/model-summary.tsx | 14 +- .../components/progress-bar.tsx | 23 +- .../training-area/open-area-map.tsx | 18 +- .../training-area/training-area-item.tsx | 46 +-- .../training-area/training-area-list.tsx | 24 +- .../training-area/training-area-map.tsx | 53 ++- .../training-area/training-area.tsx | 42 +- .../training-dataset/create-new.tsx | 9 +- .../training-dataset/select-existing.tsx | 26 +- .../training-dataset/training-dataset.tsx | 14 +- .../model-creation/hooks/use-tms-tilejson.ts | 5 +- .../hooks/use-training-areas.ts | 11 +- .../models/components/accuracy-display.tsx | 2 +- .../components/maps/training-area-map.tsx | 26 +- .../components/model-details-properties.tsx | 36 +- .../start-mapping/api/create-feedbacks.ts | 4 +- .../api/get-model-predictions.ts | 8 +- .../components/feature-popup.tsx | 96 +++-- .../start-mapping/components/header.tsx | 44 +-- .../components/logo-with-dropdown.tsx | 16 +- .../components/map/legend-control.tsx | 18 +- .../start-mapping/components/map/map.tsx | 26 +- .../components/mobile-drawer.tsx | 32 +- .../components/model-details-popup.tsx | 22 +- .../components/model-settings.tsx | 25 +- .../start-mapping/hooks/use-feedbacks.ts | 4 +- .../src/hooks/__tests__/use-login.test.ts | 82 ++++ frontend/src/hooks/use-layer-order.ts | 6 +- frontend/src/hooks/use-login.ts | 16 +- frontend/src/hooks/use-screen-size.ts | 3 + frontend/src/hooks/use-storage.ts | 3 +- frontend/src/layouts/model-forms-layout.tsx | 84 ++-- frontend/src/layouts/root-layout.tsx | 28 +- frontend/src/services/__tests__/auth.test.ts | 119 ++++++ frontend/src/services/api-client.ts | 9 +- frontend/src/services/api-routes.ts | 5 +- frontend/src/types/api.ts | 14 +- frontend/src/types/ui-contents.ts | 13 +- frontend/src/utils/geo/geo-utils.ts | 24 +- frontend/src/utils/geo/geojson-to-osm.ts | 10 +- frontend/src/utils/geo/geometry-utils.ts | 32 +- frontend/src/utils/regex-utils.ts | 2 - frontend/src/utils/string-utils.ts | 15 +- frontend/test-setup.ts | 6 + frontend/vite.config.mts | 10 + 93 files changed, 1688 insertions(+), 1027 deletions(-) create mode 100644 frontend/src/app/routes/authenticate.tsx delete mode 100644 frontend/src/app/routes/login.tsx create mode 100644 frontend/src/components/auth/auth-modal.tsx delete mode 100644 frontend/src/components/auth/osm-login-modal.tsx create mode 100644 frontend/src/constants/ui-contents/auth-content.ts create mode 100644 frontend/src/hooks/__tests__/use-login.test.ts create mode 100644 frontend/src/services/__tests__/auth.test.ts create mode 100644 frontend/test-setup.ts diff --git a/frontend/package.json b/frontend/package.json index 2c3ad6aa..6cebdb3d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -47,6 +47,7 @@ "@eslint/js": "^9.9.0", "@tailwindcss/typography": "^0.5.15", "@tanstack/eslint-plugin-query": "^5.58.1", + "@testing-library/react-hooks": "^8.0.1", "@types/geojson": "^7946.0.14", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", @@ -64,6 +65,7 @@ "eslint-plugin-react-refresh": "^0.4.9", "eslint-plugin-tailwindcss": "^3.17.5", "globals": "^15.9.0", + "jsdom": "^26.0.0", "postcss": "^8.4.47", "prettier": "3.3.3", "tailwindcss": "^3.4.12", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 2824ab11..2ec18ab0 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -106,6 +106,9 @@ importers: '@tanstack/eslint-plugin-query': specifier: ^5.58.1 version: 5.58.1(eslint@9.11.1(jiti@1.21.6))(typescript@5.6.2) + '@testing-library/react-hooks': + specifier: ^8.0.1 + version: 8.0.1(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/geojson': specifier: ^7946.0.14 version: 7946.0.14 @@ -157,6 +160,9 @@ importers: globals: specifier: ^15.9.0 version: 15.10.0 + jsdom: + specifier: ^26.0.0 + version: 26.0.0 postcss: specifier: ^8.4.47 version: 8.4.47 @@ -180,7 +186,7 @@ importers: version: 5.0.1(typescript@5.6.2)(vite@5.4.8) vitest: specifier: ^3.0.5 - version: 3.0.5(@types/debug@4.1.12) + version: 3.0.5(@types/debug@4.1.12)(jsdom@26.0.0) packages: @@ -192,6 +198,9 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} + '@asamuzakjp/css-color@2.8.3': + resolution: {integrity: sha512-GIc76d9UI1hCvOATjZPyHFmE5qhRccp3/zGfMPapK3jBi+yocEzp6BBB0UnfRYP9NP4FANqUZYb0hnfs3TM3hw==} + '@babel/code-frame@7.24.7': resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} engines: {node: '>=6.9.0'} @@ -283,6 +292,34 @@ packages: resolution: {integrity: sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==} engines: {node: '>=6.9.0'} + '@csstools/color-helpers@5.0.2': + resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.2': + resolution: {integrity: sha512-TklMyb3uBB28b5uQdxjReG4L80NxAqgrECqLZFQbyLekwwlcDDS8r3f07DKqeo8C4926Br0gf/ZDe17Zv4wIuw==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.4 + '@csstools/css-tokenizer': ^3.0.3 + + '@csstools/css-color-parser@3.0.8': + resolution: {integrity: sha512-pdwotQjCCnRPuNi06jFuP68cykU1f3ZWExLe/8MQ1LOs8Xq+fTkYgd+2V8mWUWMrOn9iS2HftPVaMZDaXzGbhQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.4 + '@csstools/css-tokenizer': ^3.0.3 + + '@csstools/css-parser-algorithms@3.0.4': + resolution: {integrity: sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.3 + + '@csstools/css-tokenizer@3.0.3': + resolution: {integrity: sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==} + engines: {node: '>=18'} + '@ctrl/tinycolor@4.1.0': resolution: {integrity: sha512-WyOx8cJQ+FQus4Mm4uPIZA64gbk3Wxh0so5Lcii0aJifqwoVOlfFtorjLE0Hen4OYyHZMXDWqMmaQemBhgxFRQ==} engines: {node: '>=14'} @@ -876,6 +913,22 @@ packages: '@terraformer/wkt@2.2.1': resolution: {integrity: sha512-XDUsW/lvbMzFi7GIuRD9+UqR4QyP+5C+TugeJLMDczKIRbaHoE9J3N8zLSdyOGmnJL9B6xTS3YMMlBnMU0Ar5A==} + '@testing-library/react-hooks@8.0.1': + resolution: {integrity: sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g==} + engines: {node: '>=12'} + peerDependencies: + '@types/react': ^16.9.0 || ^17.0.0 + react: ^16.9.0 || ^17.0.0 + react-dom: ^16.9.0 || ^17.0.0 + react-test-renderer: ^16.9.0 || ^17.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + react-dom: + optional: true + react-test-renderer: + optional: true + '@turf/area@7.1.0': resolution: {integrity: sha512-w91FEe02/mQfMPRX2pXua48scFuKJ2dSVMF2XmJ6+BJfFiCPxp95I3+Org8+ZsYv93CDNKbf0oLNEPnuQdgs2g==} @@ -1130,6 +1183,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@7.1.3: + resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} + engines: {node: '>= 14'} + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} @@ -1394,12 +1451,20 @@ packages: engines: {node: '>=4'} hasBin: true + cssstyle@4.2.1: + resolution: {integrity: sha512-9+vem03dMXG7gDmZ62uqmRiMRNtinIZ9ZyuF6BdxzfOD+FdN5hretzynkn0ReS2DO2GSw76RWHs0UmJPI2zUjw==} + engines: {node: '>=18'} + csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} engines: {node: '>= 0.4'} @@ -1430,6 +1495,9 @@ packages: supports-color: optional: true + decimal.js@10.5.0: + resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==} + decode-named-character-reference@1.0.2: resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} @@ -1491,6 +1559,10 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + es-abstract@1.23.8: resolution: {integrity: sha512-lfab8IzDn6EpI1ibZakcgS6WsfEBiB+43cuJo+wgylx1xKXf+Sp+YR3vFuQwC/u3sxYwV8Cxe3B0DpVUu/WiJQ==} engines: {node: '>= 0.4'} @@ -1518,6 +1590,10 @@ packages: resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==} engines: {node: '>= 0.4'} + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + es-shim-unscopables@1.0.2: resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==} @@ -1732,6 +1808,10 @@ packages: resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} engines: {node: '>= 6'} + form-data@4.0.2: + resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} + engines: {node: '>= 6'} + fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} @@ -1880,12 +1960,28 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + hyphenate-style-name@1.1.0: resolution: {integrity: sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==} + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -2018,6 +2114,9 @@ packages: resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} engines: {node: '>=0.10.0'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -2090,6 +2189,15 @@ packages: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true + jsdom@26.0.0: + resolution: {integrity: sha512-BZYDGVAIriBWTpIxYzrXjv3E/4u8+/pSG5bQdIYCbNCGOvsPkDQfTVLAIXAf9ETdCpduCVTkDe2NNZ8NIwUVzw==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + jsesc@2.5.2: resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} engines: {node: '>=4'} @@ -2432,6 +2540,9 @@ packages: resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} engines: {node: '>=0.10.0'} + nwsapi@2.2.16: + resolution: {integrity: sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -2490,6 +2601,9 @@ packages: parse-entities@4.0.1: resolution: {integrity: sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==} + parse5@7.2.1: + resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -2650,6 +2764,12 @@ packages: peerDependencies: react: '>= 16.8 || 18.0.0' + react-error-boundary@3.1.4: + resolution: {integrity: sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==} + engines: {node: '>=10', npm: '>=6'} + peerDependencies: + react: '>=16.13.1' + react-error-boundary@4.0.13: resolution: {integrity: sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ==} peerDependencies: @@ -2788,6 +2908,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -2806,6 +2929,13 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} @@ -2975,6 +3105,9 @@ packages: resolution: {integrity: sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==} engines: {node: '>=0.10.0'} + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + synckit@0.9.1: resolution: {integrity: sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==} engines: {node: ^14.18.0 || >=16.0.0} @@ -3033,6 +3166,13 @@ packages: resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} + tldts-core@6.1.78: + resolution: {integrity: sha512-jS0svNsB99jR6AJBmfmEWuKIgz91Haya91Z43PATaeHJ24BkMoNRb/jlaD37VYjb0mYf6gRL/HOnvS1zEnYBiw==} + + tldts@6.1.78: + resolution: {integrity: sha512-fSgYrW0ITH0SR/CqKMXIruYIPpNu5aDgUp22UhYoSrnUQwc7SBqifEBFNce7AAcygUPBo6a/gbtcguWdmko4RQ==} + hasBin: true + to-fast-properties@2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} @@ -3041,6 +3181,14 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + tough-cookie@5.1.1: + resolution: {integrity: sha512-Ek7HndSVkp10hmHP9V4qZO1u+pn1RU5sI0Fw+jCU3lyvuMZcgqsNgc6CmJJZyByK4Vm/qotGRJlfgAX8q+4JiA==} + engines: {node: '>=16'} + + tr46@5.0.0: + resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==} + engines: {node: '>=18'} + trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -3254,6 +3402,26 @@ packages: vt-pbf@3.1.3: resolution: {integrity: sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==} + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.1.1: + resolution: {integrity: sha512-mDGf9diDad/giZ/Sm9Xi2YcyzaFpbdLpJPr+E9fSkyQ7KpQD4SdFcugkRQYzhmfI4KeV4Qpnn2sKPdo+kmsgRQ==} + engines: {node: '>=18'} + which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -3297,10 +3465,29 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} + ws@8.18.1: + resolution: {integrity: sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + xmlbuilder2@3.1.1: resolution: {integrity: sha512-WCSfbfZnQDdLQLiMdGUQpMxxckeQ4oZNMNhLVkcekTu7xhD4tuUDyAPoY8CwXvBYE6LwBHd6QW2WZXlOWr1vCw==} engines: {node: '>=12.0'} + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -3325,6 +3512,14 @@ snapshots: '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 + '@asamuzakjp/css-color@2.8.3': + dependencies: + '@csstools/css-calc': 2.1.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-color-parser': 3.0.8(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 + lru-cache: 10.4.3 + '@babel/code-frame@7.24.7': dependencies: '@babel/highlight': 7.24.7 @@ -3453,6 +3648,26 @@ snapshots: '@babel/helper-validator-identifier': 7.24.7 to-fast-properties: 2.0.0 + '@csstools/color-helpers@5.0.2': {} + + '@csstools/css-calc@2.1.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 + + '@csstools/css-color-parser@3.0.8(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': + dependencies: + '@csstools/color-helpers': 5.0.2 + '@csstools/css-calc': 2.1.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 + + '@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3)': + dependencies: + '@csstools/css-tokenizer': 3.0.3 + + '@csstools/css-tokenizer@3.0.3': {} + '@ctrl/tinycolor@4.1.0': {} '@emotion/is-prop-valid@0.7.3': @@ -3927,6 +4142,15 @@ snapshots: '@terraformer/wkt@2.2.1': {} + '@testing-library/react-hooks@8.0.1(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.25.6 + react: 18.3.1 + react-error-boundary: 3.1.4(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.10 + react-dom: 18.3.1(react@18.3.1) + '@turf/area@7.1.0': dependencies: '@turf/helpers': 7.1.0 @@ -4298,6 +4522,8 @@ snapshots: acorn@8.12.1: {} + agent-base@7.1.3: {} + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -4583,10 +4809,20 @@ snapshots: cssesc@3.0.0: {} + cssstyle@4.2.1: + dependencies: + '@asamuzakjp/css-color': 2.8.3 + rrweb-cssom: 0.8.0 + csstype@3.1.3: {} damerau-levenshtein@1.0.8: {} + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.1.1 + data-view-buffer@1.0.2: dependencies: call-bound: 1.0.3 @@ -4613,6 +4849,8 @@ snapshots: dependencies: ms: 2.1.3 + decimal.js@10.5.0: {} + decode-named-character-reference@1.0.2: dependencies: character-entities: 2.0.2 @@ -4667,6 +4905,8 @@ snapshots: emoji-regex@9.2.2: {} + entities@4.5.0: {} + es-abstract@1.23.8: dependencies: array-buffer-byte-length: 1.0.2 @@ -4754,6 +4994,13 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.2 + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.2.6 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + es-shim-unscopables@1.0.2: dependencies: hasown: 2.0.2 @@ -5023,6 +5270,13 @@ snapshots: combined-stream: 1.0.8 mime-types: 2.1.35 + form-data@4.0.2: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + mime-types: 2.1.35 + fraction.js@4.3.7: {} framer-motion@11.9.0(@emotion/is-prop-valid@0.7.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): @@ -5174,10 +5428,32 @@ snapshots: dependencies: react-is: 16.13.1 + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + html-url-attributes@3.0.1: {} + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.3 + debug: 4.4.0 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.3 + debug: 4.4.0 + transitivePeerDependencies: + - supports-color + hyphenate-style-name@1.1.0: {} + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -5295,6 +5571,8 @@ snapshots: dependencies: isobject: 3.0.1 + is-potential-custom-element-name@1.0.1: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.3 @@ -5370,6 +5648,34 @@ snapshots: dependencies: argparse: 2.0.1 + jsdom@26.0.0: + dependencies: + cssstyle: 4.2.1 + data-urls: 5.0.0 + decimal.js: 10.5.0 + form-data: 4.0.2 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.16 + parse5: 7.2.1 + rrweb-cssom: 0.8.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.1.1 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.1.1 + ws: 8.18.1 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + jsesc@2.5.2: {} json-buffer@3.0.1: {} @@ -5974,6 +6280,8 @@ snapshots: normalize-range@0.1.2: {} + nwsapi@2.2.16: {} + object-assign@4.1.1: {} object-hash@3.0.0: {} @@ -6051,6 +6359,10 @@ snapshots: is-decimal: 2.0.1 is-hexadecimal: 2.0.1 + parse5@7.2.1: + dependencies: + entities: 4.5.0 + path-exists@4.0.0: {} path-key@3.1.1: {} @@ -6183,6 +6495,11 @@ snapshots: prop-types: 15.8.1 react: 18.3.1 + react-error-boundary@3.1.4(react@18.3.1): + dependencies: + '@babel/runtime': 7.25.6 + react: 18.3.1 + react-error-boundary@4.0.13(react@18.3.1): dependencies: '@babel/runtime': 7.25.6 @@ -6385,6 +6702,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.23.0 fsevents: 2.3.3 + rrweb-cssom@0.8.0: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -6410,6 +6729,12 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 + safer-buffer@2.1.2: {} + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + scheduler@0.23.2: dependencies: loose-envify: 1.4.0 @@ -6621,6 +6946,8 @@ snapshots: symbol-observable@1.2.0: {} + symbol-tree@3.2.4: {} + synckit@0.9.1: dependencies: '@pkgr/core': 0.1.1 @@ -6691,12 +7018,26 @@ snapshots: tinyspy@3.0.2: {} + tldts-core@6.1.78: {} + + tldts@6.1.78: + dependencies: + tldts-core: 6.1.78 + to-fast-properties@2.0.0: {} to-regex-range@5.0.1: dependencies: is-number: 7.0.0 + tough-cookie@5.1.1: + dependencies: + tldts: 6.1.78 + + tr46@5.0.0: + dependencies: + punycode: 2.3.1 + trim-lines@3.0.1: {} trough@2.2.0: {} @@ -6899,7 +7240,7 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - vitest@3.0.5(@types/debug@4.1.12): + vitest@3.0.5(@types/debug@4.1.12)(jsdom@26.0.0): dependencies: '@vitest/expect': 3.0.5 '@vitest/mocker': 3.0.5(vite@5.4.8) @@ -6923,6 +7264,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 + jsdom: 26.0.0 transitivePeerDependencies: - less - lightningcss @@ -6940,6 +7282,23 @@ snapshots: '@mapbox/vector-tile': 1.3.1 pbf: 3.3.0 + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@7.0.0: {} + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-url@14.1.1: + dependencies: + tr46: 5.0.0 + webidl-conversions: 7.0.0 + which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 @@ -7007,6 +7366,10 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.1.0 + ws@8.18.1: {} + + xml-name-validator@5.0.0: {} + xmlbuilder2@3.1.1: dependencies: '@oozcitak/dom': 1.15.10 @@ -7014,6 +7377,8 @@ snapshots: '@oozcitak/util': 8.3.8 js-yaml: 3.14.1 + xmlchars@2.2.0: {} + yallist@3.1.1: {} yaml@2.5.1: {} diff --git a/frontend/src/app/providers/auth-provider.tsx b/frontend/src/app/providers/auth-provider.tsx index 83983829..c2b1824a 100644 --- a/frontend/src/app/providers/auth-provider.tsx +++ b/frontend/src/app/providers/auth-provider.tsx @@ -1,20 +1,15 @@ -import React, { - createContext, - useContext, - useEffect, - useState - } from 'react'; -import { apiClient } from '@/services/api-client'; -import { authService } from '@/services'; -import { HOT_FAIR_LOCAL_STORAGE_ACCESS_TOKEN_KEY, HOT_FAIR_LOGIN_SUCCESSFUL_SESSION_KEY, HOT_FAIR_SESSION_REDIRECT_KEY } from '@/config'; -import { showErrorToast, showSuccessToast } from '@/utils'; -import { TUser } from '@/types/api'; -import { useLocalStorage, useSessionStorage } from '@/hooks/use-storage'; +import React, { createContext, useContext, useEffect, useState } from "react"; +import { apiClient } from "@/services/api-client"; +import { authService } from "@/services"; import { - TOAST_NOTIFICATIONS, -} from "@/constants"; - - + HOT_FAIR_LOCAL_STORAGE_ACCESS_TOKEN_KEY, + HOT_FAIR_LOGIN_SUCCESSFUL_SESSION_KEY, + HOT_FAIR_SESSION_REDIRECT_KEY, +} from "@/config"; +import { showErrorToast, showSuccessToast } from "@/utils"; +import { TUser } from "@/types/api"; +import { useLocalStorage, useSessionStorage } from "@/hooks/use-storage"; +import { TOAST_NOTIFICATIONS } from "@/constants"; type TAuthContext = { token: string; @@ -41,11 +36,8 @@ type AuthProviderProps = { export const AuthProvider: React.FC = ({ children }) => { const { getValue, setValue, removeValue } = useLocalStorage(); - const { - getSessionValue, - removeSessionValue, - setSessionValue, - } = useSessionStorage(); + const { getSessionValue, removeSessionValue, setSessionValue } = + useSessionStorage(); const [token, setToken] = useState( getValue(HOT_FAIR_LOCAL_STORAGE_ACCESS_TOKEN_KEY), @@ -58,7 +50,6 @@ export const AuthProvider: React.FC = ({ children }) => { // Set token globally to eliminate the need to rewrite it apiClient.defaults.headers.common["access-token"] = token ? `${token}` : null; - const handleRedirection = () => { const redirectTo = getSessionValue(HOT_FAIR_SESSION_REDIRECT_KEY); if (redirectTo) { @@ -113,7 +104,6 @@ export const AuthProvider: React.FC = ({ children }) => { } }, [token]); - /** * Clean up and logout. */ diff --git a/frontend/src/app/providers/models-provider.tsx b/frontend/src/app/providers/models-provider.tsx index f3affbc2..a6c66ab5 100644 --- a/frontend/src/app/providers/models-provider.tsx +++ b/frontend/src/app/providers/models-provider.tsx @@ -2,17 +2,17 @@ import { APPLICATION_ROUTES, MODELS_BASE, MODELS_ROUTES, - TOAST_NOTIFICATIONS - } from '@/constants'; -import { BASE_MODELS, TrainingDatasetOption, TrainingType } from '@/enums'; -import { HOT_FAIR_MODEL_CREATION_SESSION_STORAGE_KEY } from '@/config'; -import { LngLatBoundsLike } from 'maplibre-gl'; -import { useCreateTrainingDataset } from '@/features/model-creation/hooks/use-training-datasets'; -import { useGetTrainingDataset } from '@/features/models/hooks/use-dataset'; -import { useLocation, useNavigate, useParams } from 'react-router-dom'; -import { useModelDetails } from '@/features/models/hooks/use-models'; -import { UseMutationResult } from '@tanstack/react-query'; -import { useSessionStorage } from '@/hooks/use-storage'; + TOAST_NOTIFICATIONS, +} from "@/constants"; +import { BASE_MODELS, TrainingDatasetOption, TrainingType } from "@/enums"; +import { HOT_FAIR_MODEL_CREATION_SESSION_STORAGE_KEY } from "@/config"; +import { LngLatBoundsLike } from "maplibre-gl"; +import { useCreateTrainingDataset } from "@/features/model-creation/hooks/use-training-datasets"; +import { useGetTrainingDataset } from "@/features/models/hooks/use-dataset"; +import { useLocation, useNavigate, useParams } from "react-router-dom"; +import { useModelDetails } from "@/features/models/hooks/use-models"; +import { UseMutationResult } from "@tanstack/react-query"; +import { useSessionStorage } from "@/hooks/use-storage"; import { TTrainingAreaFeature, @@ -231,8 +231,8 @@ const ModelsContext = createContext<{ validateEditMode: boolean; }>({ formData: initialFormState, - setFormData: () => { }, - handleChange: () => { }, + setFormData: () => {}, + handleChange: () => {}, createNewTrainingDatasetMutation: {} as UseMutationResult< TTrainingDataset, Error, @@ -247,13 +247,13 @@ const ModelsContext = createContext<{ >, hasLabeledTrainingAreas: false, hasAOIsWithGeometry: false, - resetState: () => { }, + resetState: () => {}, isEditMode: false, modelId: "", getFullPath: () => "", - handleModelCreationAndUpdate: () => { }, + handleModelCreationAndUpdate: () => {}, trainingDatasetCreationInProgress: false, - handleTrainingDatasetCreation: () => { }, + handleTrainingDatasetCreation: () => {}, validateEditMode: false, }); @@ -263,14 +263,16 @@ export const ModelsProvider: React.FC<{ const navigate = useNavigate(); const { pathname } = useLocation(); const { modelId } = useParams(); - const { getSessionValue, setSessionValue, removeSessionValue } = useSessionStorage(); + const { getSessionValue, setSessionValue, removeSessionValue } = + useSessionStorage(); - const storedFormData = getSessionValue(HOT_FAIR_MODEL_CREATION_SESSION_STORAGE_KEY); + const storedFormData = getSessionValue( + HOT_FAIR_MODEL_CREATION_SESSION_STORAGE_KEY, + ); const [formData, setFormData] = useState( - storedFormData ? JSON.parse(storedFormData) : initialFormState + storedFormData ? JSON.parse(storedFormData) : initialFormState, ); - const handleChange = ( field: string, value: @@ -283,7 +285,10 @@ export const ModelsProvider: React.FC<{ ) => { setFormData((prev) => { const updatedData = { ...prev, [field]: value }; - setSessionValue(HOT_FAIR_MODEL_CREATION_SESSION_STORAGE_KEY, JSON.stringify(updatedData)); + setSessionValue( + HOT_FAIR_MODEL_CREATION_SESSION_STORAGE_KEY, + JSON.stringify(updatedData), + ); return updatedData; }); }; @@ -362,7 +367,6 @@ export const ModelsProvider: React.FC<{ }; }, []); - const resetState = () => { removeSessionValue(HOT_FAIR_MODEL_CREATION_SESSION_STORAGE_KEY); setFormData(initialFormState); @@ -456,7 +460,6 @@ export const ModelsProvider: React.FC<{ (aoi: TTrainingAreaFeature) => aoi.geometry === null, ).length === 0; - const handleTrainingDatasetCreation = () => { createNewTrainingDatasetMutation.mutate({ source_imagery: formData.tmsURL, diff --git a/frontend/src/app/router.tsx b/frontend/src/app/router.tsx index c13942cf..87102617 100644 --- a/frontend/src/app/router.tsx +++ b/frontend/src/app/router.tsx @@ -1,7 +1,7 @@ -import { APPLICATION_ROUTES } from '@/constants'; -import { MainErrorFallback } from '@/components/errors'; -import { ModelFormsLayout, RootLayout } from '@/layouts'; -import { ProtectedRoute } from '@/app/routes/protected-route'; +import { APPLICATION_ROUTES } from "@/constants"; +import { MainErrorFallback } from "@/components/errors"; +import { ModelFormsLayout, RootLayout } from "@/layouts"; +import { ProtectedRoute } from "@/app/routes/protected-route"; import { Navigate, RouterProvider, @@ -325,10 +325,12 @@ const router = createBrowserRouter([ * Auth route */ { - path: APPLICATION_ROUTES.LOGIN, + path: APPLICATION_ROUTES.AUTH_CALLBACK, lazy: async () => { - const { LoginPage } = await import("@/app/routes/login"); - return { Component: LoginPage }; + const { AuthenticationCallbackPage } = await import( + "@/app/routes/authenticate" + ); + return { Component: AuthenticationCallbackPage }; }, }, diff --git a/frontend/src/app/routes/authenticate.tsx b/frontend/src/app/routes/authenticate.tsx new file mode 100644 index 00000000..4900c03c --- /dev/null +++ b/frontend/src/app/routes/authenticate.tsx @@ -0,0 +1,32 @@ +import { AuthenticationModal } from "@/components/auth"; +import { Head } from "@/components/seo"; +import { AUTH_PAGE_AND_MODAL_CONTENT } from "@/constants/ui-contents/auth-content"; +import { useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import { useAuth } from "@/app/providers/auth-provider"; +import { APPLICATION_ROUTES } from "@/constants"; + +export const AuthenticationCallbackPage = () => { + const navigate = useNavigate(); + const { isAuthenticated } = useAuth(); + + useEffect(() => { + const params = new URLSearchParams(window.location.search); + const code = params.get("code"); + const state = params.get("state"); + /** + * Redirect any visit to this page back to the homepage, + * if there is no code and state in the url, or if the user is authenticated already. + */ + if (isAuthenticated || !code || !state) { + navigate(APPLICATION_ROUTES.HOMEPAGE); + } + }, [isAuthenticated]); + + return ( + <> + + + + ); +}; diff --git a/frontend/src/app/routes/learn.tsx b/frontend/src/app/routes/learn.tsx index db46bce5..806d9795 100644 --- a/frontend/src/app/routes/learn.tsx +++ b/frontend/src/app/routes/learn.tsx @@ -1,4 +1,4 @@ -import { PageUnderConstruction } from '@/components/errors'; +import { PageUnderConstruction } from "@/components/errors"; // import { Button } from "@/components/ui/button"; // import { ExternalLinkIcon, YouTubePlayCircleIcon } from "@/components/ui/icons"; // import { fAIrValues } from "@/assets/svgs"; diff --git a/frontend/src/app/routes/login.tsx b/frontend/src/app/routes/login.tsx deleted file mode 100644 index d55ff09c..00000000 --- a/frontend/src/app/routes/login.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { OSMLogo, } from "@/assets/images"; - -import { NavLogo } from "@/components/layout" -import { MadeWithLove } from "@/components/shared"; -import { Dialog } from "@/components/ui/dialog"; -import { Image } from "@/components/ui/image"; -import { useDialog } from "@/hooks/use-dialog"; -import { useLogin } from "@/hooks/use-login"; -import { useLocation, useNavigate } from "react-router-dom"; - - - -export const LoginPage = () => { - - const { closeDialog } = useDialog(); - const navigate = useNavigate(); - const { handleLogin } = useLogin(); - const location = useLocation(); - - console.log(location.state); - - const handleOnClose = () => { - closeDialog(); - navigate(-1); - }; - - return ( - -
- -

Welcome to fAIr

- - -
-
- ); -} \ No newline at end of file diff --git a/frontend/src/app/routes/models/model-details-form.tsx b/frontend/src/app/routes/models/model-details-form.tsx index 5727c6cb..1af08216 100644 --- a/frontend/src/app/routes/models/model-details-form.tsx +++ b/frontend/src/app/routes/models/model-details-form.tsx @@ -1,4 +1,4 @@ -import { ModelDetailsForm } from '@/features/model-creation/components/'; +import { ModelDetailsForm } from "@/features/model-creation/components/"; export const ModelDetailsFormPage = () => { return ( diff --git a/frontend/src/app/routes/models/training-dataset.tsx b/frontend/src/app/routes/models/training-dataset.tsx index 0f883242..cd181654 100644 --- a/frontend/src/app/routes/models/training-dataset.tsx +++ b/frontend/src/app/routes/models/training-dataset.tsx @@ -1,4 +1,4 @@ -import { TrainingDatasetForm } from '@/features/model-creation/components'; +import { TrainingDatasetForm } from "@/features/model-creation/components"; export const ModelTrainingDatasetPage = () => { return ( diff --git a/frontend/src/app/routes/protected-route.tsx b/frontend/src/app/routes/protected-route.tsx index 4c8c62d1..9a4ccada 100644 --- a/frontend/src/app/routes/protected-route.tsx +++ b/frontend/src/app/routes/protected-route.tsx @@ -12,7 +12,7 @@ type ProtectedRouteProps = { export const ProtectedRoute: React.FC = ({ children }) => { const { isAuthenticated } = useAuth(); const navigate = useNavigate(); - const location = useLocation() + const location = useLocation(); if (!isAuthenticated) { return ( <> @@ -33,7 +33,9 @@ export const ProtectedRoute: React.FC = ({ children }) => { + )} + + + + ); +}; diff --git a/frontend/src/components/auth/index.ts b/frontend/src/components/auth/index.ts index 042530e5..539ba16a 100644 --- a/frontend/src/components/auth/index.ts +++ b/frontend/src/components/auth/index.ts @@ -1 +1 @@ -export { OpenStreetMapAuthModal } from './osm-login-modal' \ No newline at end of file +export { AuthenticationModal } from "./auth-modal"; diff --git a/frontend/src/components/auth/osm-login-modal.tsx b/frontend/src/components/auth/osm-login-modal.tsx deleted file mode 100644 index db62e0ce..00000000 --- a/frontend/src/components/auth/osm-login-modal.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { OSMLogo, } from "@/assets/images"; - -import { NavLogo } from "@/components/layout" -import { MadeWithLove } from "@/components/shared"; -import { Dialog } from "@/components/ui/dialog"; -import { Image } from "@/components/ui/image"; -import { useDialog } from "@/hooks/use-dialog"; -import { useLogin } from "@/hooks/use-login"; -import { useLocation, useNavigate } from "react-router-dom"; - - - -export const OpenStreetMapAuthModal = () => { - - const { closeDialog } = useDialog(); - const navigate = useNavigate(); - const { handleLogin } = useLogin(); - const location = useLocation(); - - console.log(location.state); - - const handleOnClose = () => { - closeDialog(); - navigate(-1); - }; - - return ( - -
- -

Welcome to fAIr

- - -
-
- ); -} \ No newline at end of file diff --git a/frontend/src/components/landing/kpi/kpi.tsx b/frontend/src/components/landing/kpi/kpi.tsx index 82320e98..0e30976e 100644 --- a/frontend/src/components/landing/kpi/kpi.tsx +++ b/frontend/src/components/landing/kpi/kpi.tsx @@ -1,8 +1,8 @@ -import styles from './kpi.module.css'; -import { API_ENDPOINTS, apiClient } from '@/services'; -import { KPI_STATS_CACHE_TIME_MS } from '@/config'; -import { SHARED_CONTENT } from '@/constants'; -import { useQuery } from '@tanstack/react-query'; +import styles from "./kpi.module.css"; +import { API_ENDPOINTS, apiClient } from "@/services"; +import { KPI_STATS_CACHE_TIME_MS } from "@/config"; +import { SHARED_CONTENT } from "@/constants"; +import { useQuery } from "@tanstack/react-query"; type TKPIS = { figure?: number; label: string; @@ -27,8 +27,6 @@ export const Kpi = () => { refetchInterval: KPI_STATS_CACHE_TIME_MS, }); - - const KPIs: TKPIS = [ { figure: data?.total_models_published ?? 0, diff --git a/frontend/src/components/layout/navbar/navbar.tsx b/frontend/src/components/layout/navbar/navbar.tsx index a72c6718..5fd1c1ea 100644 --- a/frontend/src/components/layout/navbar/navbar.tsx +++ b/frontend/src/components/layout/navbar/navbar.tsx @@ -18,7 +18,7 @@ export const NavBar = () => { const [open, setOpen] = useState(false); const { isAuthenticated } = useAuth(); const navigate = useNavigate(); - const location = useLocation() + const location = useLocation(); return ( <> @@ -40,9 +40,14 @@ export const NavBar = () => { {isAuthenticated ? ( ) : ( - )} @@ -63,9 +68,11 @@ export const NavBar = () => { @@ -80,7 +87,7 @@ export const NavBar = () => { height="20px" /> - + ); }; diff --git a/frontend/src/components/layout/navbar/user-profile.tsx b/frontend/src/components/layout/navbar/user-profile.tsx index 6aa56039..b52946d6 100644 --- a/frontend/src/components/layout/navbar/user-profile.tsx +++ b/frontend/src/components/layout/navbar/user-profile.tsx @@ -1,20 +1,15 @@ -import SlAvatar from '@shoelace-style/shoelace/dist/react/avatar/index.js'; -import styles from '@/components/layout/navbar/navbar.module.css'; -import useScreenSize from '@/hooks/use-screen-size'; -import { DropDown } from '@/components/ui/dropdown'; -import { DropdownPlacement } from '@/enums'; -import { ELEMENT_DISTANCE_FROM_NAVBAR } from '@/config'; -import { TCSSWithVars } from '@/types'; -import { truncateString } from '@/utils'; -import { useAuth } from '@/app/providers/auth-provider'; -import { useDropdownMenu } from '@/hooks/use-dropdown-menu'; -import { useNavigate } from 'react-router-dom'; -import { - APPLICATION_ROUTES, - - SHARED_CONTENT, -} from "@/constants"; - +import SlAvatar from "@shoelace-style/shoelace/dist/react/avatar/index.js"; +import styles from "@/components/layout/navbar/navbar.module.css"; +import useScreenSize from "@/hooks/use-screen-size"; +import { DropDown } from "@/components/ui/dropdown"; +import { DropdownPlacement } from "@/enums"; +import { ELEMENT_DISTANCE_FROM_NAVBAR } from "@/config"; +import { TCSSWithVars } from "@/types"; +import { truncateString } from "@/utils"; +import { useAuth } from "@/app/providers/auth-provider"; +import { useDropdownMenu } from "@/hooks/use-dropdown-menu"; +import { useNavigate } from "react-router-dom"; +import { APPLICATION_ROUTES, SHARED_CONTENT } from "@/constants"; export const UserProfile = ({ hideFullName, diff --git a/frontend/src/components/map/controls/layer-control.tsx b/frontend/src/components/map/controls/layer-control.tsx index da744fad..f964e434 100644 --- a/frontend/src/components/map/controls/layer-control.tsx +++ b/frontend/src/components/map/controls/layer-control.tsx @@ -1,11 +1,11 @@ -import { BASEMAPS, ToolTipPlacement } from '@/enums'; -import { CheckboxGroup } from '@/components/ui/form'; -import { DropDown } from '@/components/ui/dropdown'; -import { LayerStackIcon } from '@/components/ui/icons'; -import { Map } from 'maplibre-gl'; -import { ToolTip } from '@/components/ui/tooltip'; -import { useDropdownMenu } from '@/hooks/use-dropdown-menu'; -import { useEffect, useMemo, useState } from 'react'; +import { BASEMAPS, ToolTipPlacement } from "@/enums"; +import { CheckboxGroup } from "@/components/ui/form"; +import { DropDown } from "@/components/ui/dropdown"; +import { LayerStackIcon } from "@/components/ui/icons"; +import { Map } from "maplibre-gl"; +import { ToolTip } from "@/components/ui/tooltip"; +import { useDropdownMenu } from "@/hooks/use-dropdown-menu"; +import { useEffect, useMemo, useState } from "react"; import { GOOGLE_SATELLITE_BASEMAP_LAYER_ID, OSM_BASEMAP_LAYER_ID, @@ -38,12 +38,12 @@ export const LayerControl = ({ ]; const baseLayers: TBasemaps = basemaps ? [ - { value: BASEMAPS.OSM, subLayer: OSM_BASEMAP_LAYER_ID }, - { - value: BASEMAPS.GOOGLE_SATELLITE, - subLayer: GOOGLE_SATELLITE_BASEMAP_LAYER_ID, - }, - ] + { value: BASEMAPS.OSM, subLayer: OSM_BASEMAP_LAYER_ID }, + { + value: BASEMAPS.GOOGLE_SATELLITE, + subLayer: GOOGLE_SATELLITE_BASEMAP_LAYER_ID, + }, + ] : []; return { layers_, baseLayers }; }, [layers, openAerialMap, basemaps]); diff --git a/frontend/src/components/map/layers/basemaps.tsx b/frontend/src/components/map/layers/basemaps.tsx index 147bcc1a..511f5913 100644 --- a/frontend/src/components/map/layers/basemaps.tsx +++ b/frontend/src/components/map/layers/basemaps.tsx @@ -1,5 +1,5 @@ -import { Map } from 'maplibre-gl'; -import { useMapLayers } from '@/hooks/use-map-layer'; +import { Map } from "maplibre-gl"; +import { useMapLayers } from "@/hooks/use-map-layer"; import { GOOGLE_SATELLITE_BASEMAP_LAYER_ID, GOOGLE_SATELLITE_BASEMAP_SOURCE_ID, diff --git a/frontend/src/components/map/layers/open-aerial-map.tsx b/frontend/src/components/map/layers/open-aerial-map.tsx index f7816e46..95f8b9e9 100644 --- a/frontend/src/components/map/layers/open-aerial-map.tsx +++ b/frontend/src/components/map/layers/open-aerial-map.tsx @@ -1,6 +1,6 @@ -import { Map } from 'maplibre-gl'; -import { TMS_LAYER_ID, TMS_SOURCE_ID } from '@/config'; -import { useMapLayers } from '@/hooks/use-map-layer'; +import { Map } from "maplibre-gl"; +import { TMS_LAYER_ID, TMS_SOURCE_ID } from "@/config"; +import { useMapLayers } from "@/hooks/use-map-layer"; export const OpenAerialMap = ({ tileJSONURL, diff --git a/frontend/src/components/map/layers/tile-boundaries.tsx b/frontend/src/components/map/layers/tile-boundaries.tsx index c3d08e3b..ccc8ac2f 100644 --- a/frontend/src/components/map/layers/tile-boundaries.tsx +++ b/frontend/src/components/map/layers/tile-boundaries.tsx @@ -1,9 +1,9 @@ -import { GeoJSONSource, Map } from 'maplibre-gl'; -import { GeoJSONType } from '@/types'; -import { getTileBoundariesGeoJSON } from '@/utils'; -import { TILE_BOUNDARY_LAYER_ID, TILE_BOUNDARY_SOURCE_ID } from '@/config'; -import { useCallback, useEffect } from 'react'; -import { useMapLayers } from '@/hooks/use-map-layer'; +import { GeoJSONSource, Map } from "maplibre-gl"; +import { GeoJSONType } from "@/types"; +import { getTileBoundariesGeoJSON } from "@/utils"; +import { TILE_BOUNDARY_LAYER_ID, TILE_BOUNDARY_SOURCE_ID } from "@/config"; +import { useCallback, useEffect } from "react"; +import { useMapLayers } from "@/hooks/use-map-layer"; export const TileBoundaries = ({ map }: { map: Map | null }) => { useMapLayers( diff --git a/frontend/src/components/map/setups/setup-maplibre.ts b/frontend/src/components/map/setups/setup-maplibre.ts index 5698845d..e83b70c5 100644 --- a/frontend/src/components/map/setups/setup-maplibre.ts +++ b/frontend/src/components/map/setups/setup-maplibre.ts @@ -1,7 +1,7 @@ -import maplibregl, { Map } from 'maplibre-gl'; -import { BASEMAPS } from '@/enums'; -import { MAP_STYLES, MAX_ZOOM_LEVEL } from '@/config'; -import { Protocol } from 'pmtiles'; +import maplibregl, { Map } from "maplibre-gl"; +import { BASEMAPS } from "@/enums"; +import { MAP_STYLES, MAX_ZOOM_LEVEL } from "@/config"; +import { Protocol } from "pmtiles"; export const setupMaplibreMap = ( containerRef: React.RefObject, diff --git a/frontend/src/components/map/setups/setup-terra-draw.ts b/frontend/src/components/map/setups/setup-terra-draw.ts index 0a244816..3b5ab8b2 100644 --- a/frontend/src/components/map/setups/setup-terra-draw.ts +++ b/frontend/src/components/map/setups/setup-terra-draw.ts @@ -1,5 +1,5 @@ -import maplibregl from 'maplibre-gl'; -import { HexColorStyling } from 'node_modules/terra-draw/dist/common'; +import maplibregl from "maplibre-gl"; +import { HexColorStyling } from "node_modules/terra-draw/dist/common"; import { TerraDraw, TerraDrawMapLibreGLAdapter, diff --git a/frontend/src/components/shared/hot-tracking.tsx b/frontend/src/components/shared/hot-tracking.tsx index 826d645a..46a50012 100644 --- a/frontend/src/components/shared/hot-tracking.tsx +++ b/frontend/src/components/shared/hot-tracking.tsx @@ -1,9 +1,11 @@ -import { APPLICATION_ROUTES } from '@/constants'; -import { HOT_TRACKING_HTML_TAG_NAME, MATOMO_APP_DOMAIN, MATOMO_ID } from '@/config'; -import { useEffect } from 'react'; -import { useLocation } from 'react-router-dom'; - - +import { APPLICATION_ROUTES } from "@/constants"; +import { + HOT_TRACKING_HTML_TAG_NAME, + MATOMO_APP_DOMAIN, + MATOMO_ID, +} from "@/config"; +import { useEffect } from "react"; +import { useLocation } from "react-router-dom"; export const HotTracking = ({ homepagePath = APPLICATION_ROUTES.HOMEPAGE }) => { const { pathname } = useLocation(); diff --git a/frontend/src/components/shared/index.ts b/frontend/src/components/shared/index.ts index 5d1478fb..867c0d7a 100644 --- a/frontend/src/components/shared/index.ts +++ b/frontend/src/components/shared/index.ts @@ -4,4 +4,4 @@ export { SectionHeader } from "./section-header"; export * from "./pagination"; export { TheFAIRProcess } from "./fair-process/fair-process"; export { HotTracking } from "./hot-tracking"; -export { MadeWithLove } from "./made-with-love"; \ No newline at end of file +export { MadeWithLove } from "./made-with-love"; diff --git a/frontend/src/components/shared/made-with-love.tsx b/frontend/src/components/shared/made-with-love.tsx index 1e4d1498..f7e6fd31 100644 --- a/frontend/src/components/shared/made-with-love.tsx +++ b/frontend/src/components/shared/made-with-love.tsx @@ -1,27 +1,27 @@ -import { SHARED_CONTENT } from "@/constants" -import { Link } from "@/components/ui/link" +import { SHARED_CONTENT } from "@/constants"; +import { Link } from "@/components/ui/link"; export const MadeWithLove = () => { - return ( -

- {SHARED_CONTENT.footer.madeWithLove.firstSegment} - - {SHARED_CONTENT.footer.madeWithLove.secondSegment} - - {SHARED_CONTENT.footer.madeWithLove.thirdSegment} - - {SHARED_CONTENT.footer.madeWithLove.fourthSegment} - -

- ) -} \ No newline at end of file + return ( +

+ {SHARED_CONTENT.footer.madeWithLove.firstSegment} + + {SHARED_CONTENT.footer.madeWithLove.secondSegment} + + {SHARED_CONTENT.footer.madeWithLove.thirdSegment} + + {SHARED_CONTENT.footer.madeWithLove.fourthSegment} + +

+ ); +}; diff --git a/frontend/src/components/ui/dialog/dialog.css b/frontend/src/components/ui/dialog/dialog.css index 899f4af4..9b9d7541 100644 --- a/frontend/src/components/ui/dialog/dialog.css +++ b/frontend/src/components/ui/dialog/dialog.css @@ -3,6 +3,10 @@ sl-dialog::part(title) { font-weight: var(--hot-fair-font-weight-semibold); } +sl-dialog.rounded::part(panel) { + border-radius: 20px; +} + sl-dialog.primary::part(title) { color: var(--hot-fair-color-primary); } diff --git a/frontend/src/components/ui/dialog/dialog.tsx b/frontend/src/components/ui/dialog/dialog.tsx index 67f2a962..3d5393dd 100644 --- a/frontend/src/components/ui/dialog/dialog.tsx +++ b/frontend/src/components/ui/dialog/dialog.tsx @@ -10,6 +10,7 @@ type DialogProps = { children: React.ReactNode; preventClose?: boolean; labelColor?: "default" | "primary"; + borderRadius?: "rounded"; }; const Dialog: React.FC = ({ isOpened, @@ -18,6 +19,7 @@ const Dialog: React.FC = ({ children, preventClose, labelColor = "default", + borderRadius, }) => { // Prevent the dialog from closing when the user clicks on the overlay function handleRequestClose(event: any) { @@ -26,14 +28,13 @@ const Dialog: React.FC = ({ } } - const { isMobile, isTablet, isLaptop } = useScreenSize(); + const { isLaptop, isSmallViewport } = useScreenSize(); - const size = - isMobile || isTablet - ? SHOELACE_SIZES.EXTRA_LARGE - : isLaptop - ? SHOELACE_SIZES.LARGE - : SHOELACE_SIZES.MEDIUM; + const size = isSmallViewport + ? SHOELACE_SIZES.EXTRA_LARGE + : isLaptop + ? SHOELACE_SIZES.LARGE + : SHOELACE_SIZES.MEDIUM; return ( = ({ e.preventDefault(); closeDialog(); }} - className={labelColor} + className={`${labelColor} ${borderRadius}`} style={{ //@ts-expect-error bad type definition + "--width": //@ts-expect-error bad type definition size === SHOELACE_SIZES.SMALL diff --git a/frontend/src/components/ui/form/form-label/form-label.tsx b/frontend/src/components/ui/form/form-label/form-label.tsx index 346be776..a8ddc915 100644 --- a/frontend/src/components/ui/form/form-label/form-label.tsx +++ b/frontend/src/components/ui/form/form-label/form-label.tsx @@ -1,4 +1,4 @@ -import { ToolTip } from '@/components/ui/tooltip'; +import { ToolTip } from "@/components/ui/tooltip"; type FormLabelProps = { label: string; @@ -33,7 +33,9 @@ const FormLabel: React.FC = ({ {maxLength && ( - 1 && isBelowMin) ? "text-primary" : ""}`}> + 1 && isBelowMin) ? "text-primary" : ""}`} + > ({currentLength}/{maxLength}) )} diff --git a/frontend/src/components/ui/form/help-text/help-text.tsx b/frontend/src/components/ui/form/help-text/help-text.tsx index a94e4c3e..7671e5ce 100644 --- a/frontend/src/components/ui/form/help-text/help-text.tsx +++ b/frontend/src/components/ui/form/help-text/help-text.tsx @@ -1,15 +1,19 @@ - - type HelptextProps = { content?: string; isValid?: boolean; currentLength?: number; }; -const HelpText: React.FC = ({ content, isValid, currentLength }) => { - +const HelpText: React.FC = ({ + content, + isValid, + currentLength, +}) => { return ( -

0 && !isValid && 'text-primary')}`} slot="help-text"> +

0 && !isValid && "text-primary"}`} + slot="help-text" + > {content}

); diff --git a/frontend/src/components/ui/form/input/input.tsx b/frontend/src/components/ui/form/input/input.tsx index e8eed786..bf50c101 100644 --- a/frontend/src/components/ui/form/input/input.tsx +++ b/frontend/src/components/ui/form/input/input.tsx @@ -1,13 +1,12 @@ -import styles from './input.module.css'; -import useBrowserType from '@/hooks/use-browser-type'; -import useScreenSize from '@/hooks/use-screen-size'; -import { CalenderIcon } from '@/components/ui/icons'; -import { CheckIcon } from '@/components/ui/icons'; -import { FormLabel, HelpText } from '@/components/ui/form'; -import { INPUT_TYPES, SHOELACE_SIZES } from '@/enums'; -import { SlInput } from '@shoelace-style/shoelace/dist/react'; -import { useRef } from 'react'; - +import styles from "./input.module.css"; +import useBrowserType from "@/hooks/use-browser-type"; +import useScreenSize from "@/hooks/use-screen-size"; +import { CalenderIcon } from "@/components/ui/icons"; +import { CheckIcon } from "@/components/ui/icons"; +import { FormLabel, HelpText } from "@/components/ui/form"; +import { INPUT_TYPES, SHOELACE_SIZES } from "@/enums"; +import { SlInput } from "@shoelace-style/shoelace/dist/react"; +import { useRef } from "react"; type InputProps = { handleInput: (arg: React.ChangeEvent) => void; @@ -67,7 +66,7 @@ const Input: React.FC = ({ const inputRef = useRef(null); const { isMobile } = useScreenSize(); - const currentLength = String(value).length + const currentLength = String(value).length; return ( { @@ -80,7 +79,6 @@ const Input: React.FC = ({ }, ); - // @ts-expect-error bad type definition handleInput(e); }} @@ -118,7 +116,13 @@ const Input: React.FC = ({ /> )} - {helpText && } + {helpText && ( + + )} {/* We're using the native browser date picker. In chrome it displays a calender icon which unfortunately could not be customized as at 08/10/2024. diff --git a/frontend/src/components/ui/form/select/select.tsx b/frontend/src/components/ui/form/select/select.tsx index a6415f11..8be25f62 100644 --- a/frontend/src/components/ui/form/select/select.tsx +++ b/frontend/src/components/ui/form/select/select.tsx @@ -1,9 +1,9 @@ -import useScreenSize from '@/hooks/use-screen-size'; -import { FormLabel, HelpText } from '@/components/ui/form'; -import { SHOELACE_SELECT_SIZES } from '@/enums'; -import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react'; -import { TShoelaceSize } from '@/types'; -import './select.css'; +import useScreenSize from "@/hooks/use-screen-size"; +import { FormLabel, HelpText } from "@/components/ui/form"; +import { SHOELACE_SELECT_SIZES } from "@/enums"; +import { SlOption, SlSelect } from "@shoelace-style/shoelace/dist/react"; +import { TShoelaceSize } from "@/types"; +import "./select.css"; type SelectProps = { label?: string; diff --git a/frontend/src/components/ui/form/text-area/text-area.tsx b/frontend/src/components/ui/form/text-area/text-area.tsx index 95717861..a3e9f16e 100644 --- a/frontend/src/components/ui/form/text-area/text-area.tsx +++ b/frontend/src/components/ui/form/text-area/text-area.tsx @@ -1,6 +1,6 @@ -import { FormLabel, HelpText } from '@/components/ui/form'; -import { SlTextarea } from '@shoelace-style/shoelace/dist/react'; -import './text-area.css'; +import { FormLabel, HelpText } from "@/components/ui/form"; +import { SlTextarea } from "@shoelace-style/shoelace/dist/react"; +import "./text-area.css"; type TextAreaProps = { toolTipContent?: string; diff --git a/frontend/src/config/__tests__/config.test.ts b/frontend/src/config/__tests__/config.test.ts index 37d2c2da..7a32a469 100644 --- a/frontend/src/config/__tests__/config.test.ts +++ b/frontend/src/config/__tests__/config.test.ts @@ -1,61 +1,62 @@ -import { describe, expect, it } from 'vitest'; -import { parseFloatEnv, parseIntEnv, parseStringEnv } from '../index'; - +import { describe, expect, it } from "vitest"; +import { parseFloatEnv, parseIntEnv, parseStringEnv } from "../index"; describe("parseIntEnv()", () => { - it("should return the integer value when a valid number is provided", () => { - expect(parseIntEnv("10", 5)).toBe(10); - }); + it("should return the integer value when a valid number is provided", () => { + expect(parseIntEnv("10", 5)).toBe(10); + }); - it("should return the default value when input is undefined", () => { - expect(parseIntEnv(undefined, 5)).toBe(5); - }); + it("should return the default value when input is undefined", () => { + expect(parseIntEnv(undefined, 5)).toBe(5); + }); - it("should return the default value when input is NaN", () => { - expect(parseIntEnv("invalid", 5)).toBe(5); - }); + it("should return the default value when input is NaN", () => { + expect(parseIntEnv("invalid", 5)).toBe(5); + }); - it("should return 0 when input is '0'", () => { - expect(parseIntEnv("0", 5)).toBe(0); - }); + it("should return 0 when input is '0'", () => { + expect(parseIntEnv("0", 5)).toBe(0); + }); }); describe("parseFloatEnv()", () => { - it("should return the float value when a valid number is provided", () => { - expect(parseFloatEnv("10.5", 5.5)).toBe(10.5); - }); + it("should return the float value when a valid number is provided", () => { + expect(parseFloatEnv("10.5", 5.5)).toBe(10.5); + }); - it("should return the default value when input is undefined", () => { - expect(parseFloatEnv(undefined, 5.5)).toBe(5.5); - }); + it("should return the default value when input is undefined", () => { + expect(parseFloatEnv(undefined, 5.5)).toBe(5.5); + }); - it("should return the default value when input is NaN", () => { - expect(parseFloatEnv("invalid", 5.5)).toBe(5.5); - }); + it("should return the default value when input is NaN", () => { + expect(parseFloatEnv("invalid", 5.5)).toBe(5.5); + }); - it("should return 0 when input is '0'", () => { - expect(parseFloatEnv("0", 5.5)).toBe(0); - }); + it("should return 0 when input is '0'", () => { + expect(parseFloatEnv("0", 5.5)).toBe(0); + }); }); describe("parseStringEnv()", () => { - it("should return the provided value when it is a valid string", () => { - expect(parseStringEnv("example.com", "default.com")).toBe("example.com"); - }); - - it("should return the default value when input is undefined", () => { - expect(parseStringEnv(undefined, "default.com")).toBe("default.com"); - }); - - it("should return the default value when input is an empty string", () => { - expect(parseStringEnv("", "default.com")).toBe("default.com"); - }); - - it("should return the default value when input is only spaces", () => { - expect(parseStringEnv(" ", "default.com")).toBe("default.com"); - }); - - it("should trim spaces from a valid input", () => { - expect(parseStringEnv(" example.com ", "default.com")).toBe("example.com"); - }); + it("should return the provided value when it is a valid string", () => { + expect(parseStringEnv("example.com", "default.com")).toBe("example.com"); + }); + + it("should return the default value when input is undefined", () => { + expect(parseStringEnv(undefined, "default.com")).toBe("default.com"); + }); + + it("should return the default value when input is an empty string", () => { + expect(parseStringEnv("", "default.com")).toBe("default.com"); + }); + + it("should return the default value when input is only spaces", () => { + expect(parseStringEnv(" ", "default.com")).toBe("default.com"); + }); + + it("should trim spaces from a valid input", () => { + expect(parseStringEnv(" example.com ", "default.com")).toBe( + "example.com", + ); + }); }); diff --git a/frontend/src/config/env.ts b/frontend/src/config/env.ts index adeeeec8..20a60a37 100644 --- a/frontend/src/config/env.ts +++ b/frontend/src/config/env.ts @@ -1,5 +1,5 @@ /** - * The environment variables. + * The environment variables. */ export const ENVS = { BASE_API_URL: import.meta.env.VITE_BASE_API_URL, @@ -14,7 +14,8 @@ export const ENVS = { MIN_TRAINING_AREA_SIZE: import.meta.env.VITE_MIN_TRAINING_AREA_SIZE, - MAX_TRAINING_AREA_UPLOAD_FILE_SIZE: import.meta.env.VITE_MAX_TRAINING_AREA_UPLOAD_FILE_SIZE, + MAX_TRAINING_AREA_UPLOAD_FILE_SIZE: import.meta.env + .VITE_MAX_TRAINING_AREA_UPLOAD_FILE_SIZE, FAIR_VERSION: import.meta.env.VITE_FAIR_VERSION, @@ -22,37 +23,52 @@ export const ENVS = { MAX_ZOOM_LEVEL: import.meta.env.VITE_MAX_ZOOM_LEVEL, - MIN_ZOOM_LEVEL_FOR_START_MAPPING_PREDICTION: import.meta.env.VITE_MIN_ZOOM_LEVEL_FOR_START_MAPPING_PREDICTION, + MIN_ZOOM_LEVEL_FOR_START_MAPPING_PREDICTION: import.meta.env + .VITE_MIN_ZOOM_LEVEL_FOR_START_MAPPING_PREDICTION, - MIN_ZOOM_LEVEL_FOR_TRAINING_AREA_LABELS: import.meta.env.VITE_MIN_ZOOM_LEVEL_FOR_TRAINING_AREA_LABELS, + MIN_ZOOM_LEVEL_FOR_TRAINING_AREA_LABELS: import.meta.env + .VITE_MIN_ZOOM_LEVEL_FOR_TRAINING_AREA_LABELS, - TRAINING_AREAS_AOI_FILL_COLOR: import.meta.env.VITE_TRAINING_AREAS_AOI_FILL_COLOR, + TRAINING_AREAS_AOI_FILL_COLOR: import.meta.env + .VITE_TRAINING_AREAS_AOI_FILL_COLOR, - TRAINING_AREAS_AOI_OUTLINE_COLOR: import.meta.env.VITE_TRAINING_AREAS_AOI_OUTLINE_COLOR, + TRAINING_AREAS_AOI_OUTLINE_COLOR: import.meta.env + .VITE_TRAINING_AREAS_AOI_OUTLINE_COLOR, - TRAINING_AREAS_AOI_OUTLINE_WIDTH: import.meta.env.VITE_TRAINING_AREAS_AOI_OUTLINE_WIDTH, + TRAINING_AREAS_AOI_OUTLINE_WIDTH: import.meta.env + .VITE_TRAINING_AREAS_AOI_OUTLINE_WIDTH, - TRAINING_AREAS_AOI_FILL_OPACITY: import.meta.env.VITE_TRAINING_AREAS_AOI_FILL_OPACITY, + TRAINING_AREAS_AOI_FILL_OPACITY: import.meta.env + .VITE_TRAINING_AREAS_AOI_FILL_OPACITY, - TRAINING_AREAS_AOI_LABELS_FILL_OPACITY: import.meta.env.VITE_TRAINING_AREAS_AOI_LABELS_FILL_OPACITY, + TRAINING_AREAS_AOI_LABELS_FILL_OPACITY: import.meta.env + .VITE_TRAINING_AREAS_AOI_LABELS_FILL_OPACITY, - TRAINING_AREAS_AOI_LABELS_OUTLINE_WIDTH: import.meta.env.VITE_TRAINING_AREAS_AOI_LABELS_OUTLINE_WIDTH, + TRAINING_AREAS_AOI_LABELS_OUTLINE_WIDTH: import.meta.env + .VITE_TRAINING_AREAS_AOI_LABELS_OUTLINE_WIDTH, - TRAINING_AREAS_AOI_LABELS_FILL_COLOR: import.meta.env.VITE_TRAINING_AREAS_AOI_LABELS_FILL_COLOR, + TRAINING_AREAS_AOI_LABELS_FILL_COLOR: import.meta.env + .VITE_TRAINING_AREAS_AOI_LABELS_FILL_COLOR, - TRAINING_AREAS_AOI_LABELS_OUTLINE_COLOR: import.meta.env.VITE_TRAINING_AREAS_AOI_LABELS_OUTLINE_COLOR, + TRAINING_AREAS_AOI_LABELS_OUTLINE_COLOR: import.meta.env + .VITE_TRAINING_AREAS_AOI_LABELS_OUTLINE_COLOR, JOSM_REMOTE_URL: import.meta.env.VITE_JOSM_REMOTE_URL, - TRAINING_AREA_LABELS_FETCH_POOLING_INTERVAL_MS: import.meta.env.VITE_TRAINING_AREA_LABELS_FETCH_POOLING_INTERVAL_MS, + TRAINING_AREA_LABELS_FETCH_POOLING_INTERVAL_MS: import.meta.env + .VITE_TRAINING_AREA_LABELS_FETCH_POOLING_INTERVAL_MS, - OSM_LAST_UPDATED_POOLING_INTERVAL_MS: import.meta.env.VITE_OSM_LAST_UPDATED_POOLING_INTERVAL_MS, + OSM_LAST_UPDATED_POOLING_INTERVAL_MS: import.meta.env + .VITE_OSM_LAST_UPDATED_POOLING_INTERVAL_MS, - MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREA_LABELS: import.meta.env.VITE_MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREA_LABELS, + MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREA_LABELS: import.meta.env + .VITE_MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREA_LABELS, - MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREAS: import.meta.env.VITE_MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREAS, + MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREAS: import.meta.env + .VITE_MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREAS, - MAX_ACCEPTABLE_POLYGON_IN_TRAINING_AREA_GEOJSON_FILE: import.meta.env.VITE_MAX_ACCEPTABLE_POLYGON_IN_TRAINING_AREA_GEOJSON_FILE, + MAX_ACCEPTABLE_POLYGON_IN_TRAINING_AREA_GEOJSON_FILE: import.meta.env + .VITE_MAX_ACCEPTABLE_POLYGON_IN_TRAINING_AREA_GEOJSON_FILE, FAIR_PREDICTOR_API_URL: import.meta.env.VITE_FAIR_PREDICTOR_API_URL, @@ -62,4 +78,3 @@ export const ENVS = { OAM_S3_BUCKET_URL: import.meta.env.VITE_OAM_S3_BUCKET_URL, }; - diff --git a/frontend/src/config/index.ts b/frontend/src/config/index.ts index 960c3f98..81c9cde3 100644 --- a/frontend/src/config/index.ts +++ b/frontend/src/config/index.ts @@ -1,160 +1,193 @@ -import { BASE_MODELS } from '@/enums'; -import { ENVS } from '@/config/env'; -import { StyleSpecification } from 'maplibre-gl'; - +import { BASE_MODELS } from "@/enums"; +import { ENVS } from "@/config/env"; +import { StyleSpecification } from "maplibre-gl"; // ============================================================================================================================== // Helper functions // ============================================================================================================================== - - /** * Helper function to safely parse environment variables as integers. */ -export const parseIntEnv = (value: string | undefined, defaultValue: number): number => - value !== undefined && !isNaN(parseInt(value, 10)) ? parseInt(value, 10) : defaultValue; +export const parseIntEnv = ( + value: string | undefined, + defaultValue: number, +): number => + value !== undefined && !isNaN(parseInt(value, 10)) + ? parseInt(value, 10) + : defaultValue; /** * Helper function to safely parse environment variables as floats. */ -export const parseFloatEnv = (value: string | undefined, defaultValue: number): number => - value !== undefined && !isNaN(parseFloat(value)) ? parseFloat(value) : defaultValue; +export const parseFloatEnv = ( + value: string | undefined, + defaultValue: number, +): number => + value !== undefined && !isNaN(parseFloat(value)) + ? parseFloat(value) + : defaultValue; /** * Helper function to safely parse environment variables as strings. */ -export const parseStringEnv = (value: string | undefined, defaultValue: string): string => - value && value.trim() !== "" ? value.trim() : defaultValue; - +export const parseStringEnv = ( + value: string | undefined, + defaultValue: string, +): string => (value && value.trim() !== "" ? value.trim() : defaultValue); // ============================================================================================================================== // API Endpoints // ============================================================================================================================== - - /** * The backend api endpoint url. * Note: Ensure CORs is enabled in the backend and access is given to your port. -*/ -export const BASE_API_URL: string = parseStringEnv(ENVS.BASE_API_URL, "http://localhost:8000/api/v1/"); + */ +export const BASE_API_URL: string = parseStringEnv( + ENVS.BASE_API_URL, + "http://localhost:8000/api/v1/", +); /** * The Base URL for OAM's Titiler. -*/ -export const OAM_TITILER_ENDPOINT: string = parseStringEnv(ENVS.OAM_TITILER_ENDPOINT, "https://titiler.hotosm.org/"); + */ +export const OAM_TITILER_ENDPOINT: string = parseStringEnv( + ENVS.OAM_TITILER_ENDPOINT, + "https://titiler.hotosm.org/", +); /** * The new S3 bucket for OAM aerial imageries. -*/ -export const OAM_S3_BUCKET_URL: string = parseStringEnv(ENVS.OAM_S3_BUCKET_URL, "https://oin-hotosm-temp.s3.us-east-1.amazonaws.com/"); + */ +export const OAM_S3_BUCKET_URL: string = parseStringEnv( + ENVS.OAM_S3_BUCKET_URL, + "https://oin-hotosm-temp.s3.us-east-1.amazonaws.com/", +); /** * The remote url to JOSM. */ -export const JOSM_REMOTE_URL: string = parseStringEnv(ENVS.JOSM_REMOTE_URL, "http://127.0.0.1:8111/"); +export const JOSM_REMOTE_URL: string = parseStringEnv( + ENVS.JOSM_REMOTE_URL, + "http://127.0.0.1:8111/", +); /** * The OSM Database status API endpoint. */ -export const OSM_DATABASE_STATUS_API_ENDPOINT: string = parseStringEnv(ENVS.OSM_DATABASE_STATUS_API_URL, "https://api-prod.raw-data.hotosm.org/v1/status/"); +export const OSM_DATABASE_STATUS_API_ENDPOINT: string = parseStringEnv( + ENVS.OSM_DATABASE_STATUS_API_URL, + "https://api-prod.raw-data.hotosm.org/v1/status/", +); /** * The model prediction endpoint. */ -export const FAIR_PREDICTOR_API_ENDPOINT: string = parseStringEnv(ENVS.FAIR_PREDICTOR_API_URL, "https://predictor-dev.fair.hotosm.org/predict/"); - - +export const FAIR_PREDICTOR_API_ENDPOINT: string = parseStringEnv( + ENVS.FAIR_PREDICTOR_API_URL, + "https://predictor-dev.fair.hotosm.org/predict/", +); // ============================================================================================================================== // Local & Session Storage Keys // ============================================================================================================================== - - /** * The key used to store the access token in local storage for the application. */ -export const HOT_FAIR_LOCAL_STORAGE_ACCESS_TOKEN_KEY: string = "___hot_fAIr_access_token"; +export const HOT_FAIR_LOCAL_STORAGE_ACCESS_TOKEN_KEY: string = + "___hot_fAIr_access_token"; /** * The key used to store the redirect URL after login in session storage for the application. */ -export const HOT_FAIR_SESSION_REDIRECT_KEY: string = "___hot_fAIr_redirect_after_login"; +export const HOT_FAIR_SESSION_REDIRECT_KEY: string = + "___hot_fAIr_redirect_after_login"; /** * The key used to indicate a successful login session for the application. */ -export const HOT_FAIR_LOGIN_SUCCESSFUL_SESSION_KEY: string = "__hot_fair_login_successful"; +export const HOT_FAIR_LOGIN_SUCCESSFUL_SESSION_KEY: string = + "__hot_fair_login_successful"; /** * The key used to store the model form data in session storage to preserve the state incase the user * visits ID Editor or JOSM to map a training area. * Session storage is used to allow users to be able to open fAIr on a new tab and start on a clean slate. */ -export const HOT_FAIR_MODEL_CREATION_SESSION_STORAGE_KEY: string = "__hot_fair_model_creation_formdata"; +export const HOT_FAIR_MODEL_CREATION_SESSION_STORAGE_KEY: string = + "__hot_fair_model_creation_formdata"; /** * The key used to store the banner state in local storage for the application. */ -export const HOT_FAIR_BANNER_LOCAL_STORAGE_KEY: string = "__hot_fair_banner_closed"; +export const HOT_FAIR_BANNER_LOCAL_STORAGE_KEY: string = + "__hot_fair_banner_closed"; /** * The key used to store the model predictions in the session storage for the application. */ -export const HOT_FAIR_MODEL_PREDICTIONS_SESSION_STORAGE_KEY: string = "__hot_fair_model_predictions"; - - +export const HOT_FAIR_MODEL_PREDICTIONS_SESSION_STORAGE_KEY: string = + "__hot_fair_model_predictions"; // ============================================================================================================================== // Training Area Configurations // ============================================================================================================================== - /** * The maximum allowed area size (in square meters) for training areas. */ -export const MAX_TRAINING_AREA_SIZE: number = parseIntEnv(ENVS.MAX_TRAINING_AREA_SIZE, 5000000); +export const MAX_TRAINING_AREA_SIZE: number = parseIntEnv( + ENVS.MAX_TRAINING_AREA_SIZE, + 5000000, +); /** * The minimum allowed area size (in square meters) for training areas. * The default is set to 5797 sq. meters (1.43 acres). */ -export const MIN_TRAINING_AREA_SIZE: number = parseIntEnv(ENVS.MIN_TRAINING_AREA_SIZE, 5797); +export const MIN_TRAINING_AREA_SIZE: number = parseIntEnv( + ENVS.MIN_TRAINING_AREA_SIZE, + 5797, +); /** * The maximum file size (in bytes) allowed for training area upload. * The default is set to 5 MB. */ -export const MAX_TRAINING_AREA_UPLOAD_FILE_SIZE: number = parseIntEnv(ENVS.MAX_TRAINING_AREA_UPLOAD_FILE_SIZE, 5 * 1024 * 1024); +export const MAX_TRAINING_AREA_UPLOAD_FILE_SIZE: number = parseIntEnv( + ENVS.MAX_TRAINING_AREA_UPLOAD_FILE_SIZE, + 5 * 1024 * 1024, +); /** * The maximum GeoJSON file(s) containing the training labels, a user can upload for an AOI/Training Area. * Default value: 1 (1 GeoJSON file). -*/ -export const MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREA_LABELS: number = parseIntEnv(ENVS.MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREA_LABELS, 1); + */ +export const MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREA_LABELS: number = + parseIntEnv(ENVS.MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREA_LABELS, 1); /** * The maximum GeoJSON file(s) containing the training areas/AOI polygon geometry that a user can upload. * Default value: 10 (10 GeoJSON files, assumming each file has a single AOI). -*/ -export const MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREAS: number = parseIntEnv(ENVS.MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREAS, 10); + */ +export const MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREAS: number = parseIntEnv( + ENVS.MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREAS, + 10, +); /** * The maximum polygon geometry a single training area GeoJSON file can contain. * Default value: 10 (10 polygon geometries). -*/ -export const MAX_ACCEPTABLE_POLYGON_IN_TRAINING_AREA_GEOJSON_FILE: number = parseIntEnv(ENVS.MAX_ACCEPTABLE_POLYGON_IN_TRAINING_AREA_GEOJSON_FILE, 10); - - + */ +export const MAX_ACCEPTABLE_POLYGON_IN_TRAINING_AREA_GEOJSON_FILE: number = + parseIntEnv(ENVS.MAX_ACCEPTABLE_POLYGON_IN_TRAINING_AREA_GEOJSON_FILE, 10); // ============================================================================================================================== // Map Configurations // ============================================================================================================================== - - /** * The maximum zoom level for the map. */ @@ -163,7 +196,10 @@ export const MAX_ZOOM_LEVEL: number = parseIntEnv(ENVS.MAX_ZOOM_LEVEL, 22); /** * The minimum zoom level for the map before the prediction components can be activated. */ -export const MIN_ZOOM_LEVEL_FOR_START_MAPPING_PREDICTION: number = parseIntEnv(ENVS.MIN_ZOOM_LEVEL_FOR_START_MAPPING_PREDICTION, 19); +export const MIN_ZOOM_LEVEL_FOR_START_MAPPING_PREDICTION: number = parseIntEnv( + ENVS.MIN_ZOOM_LEVEL_FOR_START_MAPPING_PREDICTION, + 19, +); /** * The instruction to show the users when they haven't reach the minimum zoom level on the start mapping page. @@ -178,23 +214,22 @@ export const MAP_STYLES_PREFIX: string = "fAIr"; /** * The minimum zoom level to show the training area labels. */ -export const MIN_ZOOM_LEVEL_FOR_TRAINING_AREA_LABELS: number = parseIntEnv(ENVS.MIN_ZOOM_LEVEL_FOR_TRAINING_AREA_LABELS, 18); +export const MIN_ZOOM_LEVEL_FOR_TRAINING_AREA_LABELS: number = parseIntEnv( + ENVS.MIN_ZOOM_LEVEL_FOR_TRAINING_AREA_LABELS, + 18, +); /** * OSM Basemap style. -*/ + */ export const MAP_STYLES: Record = { - OSM: "https://tiles.openfreemap.org/styles/bright", + OSM: "https://tiles.openfreemap.org/styles/bright", }; - - // ============================================================================================================================== // Layers, Sources and Name Mappings // ============================================================================================================================== - - // Shared (Basemaps, Tile Boundaries) export const TILE_BOUNDARY_LAYER_ID: string = `${MAP_STYLES_PREFIX}-tile-boundary-layer`; export const TILE_BOUNDARY_SOURCE_ID: string = `${MAP_STYLES_PREFIX}-tile-boundaries`; @@ -205,40 +240,66 @@ export const GOOGLE_SATELLITE_BASEMAP_LAYER_ID: string = `${MAP_STYLES_PREFIX}-g export const GOOGLE_SATELLITE_BASEMAP_SOURCE_ID: string = `${MAP_STYLES_PREFIX}-google-satellite`; // Start Mapping -export const ACCEPTED_MODEL_PREDICTIONS_SOURCE_ID: string = "accepted-predictions-source"; +export const ACCEPTED_MODEL_PREDICTIONS_SOURCE_ID: string = + "accepted-predictions-source"; export const ACCEPTED_MODEL_PREDICTIONS_FILL_LAYER_ID: string = `${MAP_STYLES_PREFIX}-accepted-predictions-fill-layer`; -export const ACCEPTED_MODEL_PREDICTIONS_OUTLINE_LAYER_ID: string = "accepted-predictions-outline-layer"; +export const ACCEPTED_MODEL_PREDICTIONS_OUTLINE_LAYER_ID: string = + "accepted-predictions-outline-layer"; export const ALL_MODEL_PREDICTIONS_SOURCE_ID: string = "all-predictions-source"; export const ALL_MODEL_PREDICTIONS_FILL_LAYER_ID: string = `${MAP_STYLES_PREFIX}-all-predictions-fill-layer`; -export const ALL_MODEL_PREDICTIONS_OUTLINE_LAYER_ID: string = "all-predictions-outline-layer"; -export const REJECTED_MODEL_PREDICTIONS_SOURCE_ID: string = "rejected-predictions-source"; +export const ALL_MODEL_PREDICTIONS_OUTLINE_LAYER_ID: string = + "all-predictions-outline-layer"; +export const REJECTED_MODEL_PREDICTIONS_SOURCE_ID: string = + "rejected-predictions-source"; export const REJECTED_MODEL_PREDICTIONS_FILL_LAYER_ID: string = `${MAP_STYLES_PREFIX}-rejected-predictions-fill-layer`; -export const REJECTED_MODEL_PREDICTIONS_OUTLINE_LAYER_ID: string = "rejected-predictions-outline-layer"; +export const REJECTED_MODEL_PREDICTIONS_OUTLINE_LAYER_ID: string = + "rejected-predictions-outline-layer"; // Training Areas -export const TRAINING_AREAS_AOI_FILL_COLOR: string = parseStringEnv(ENVS.TRAINING_AREAS_AOI_FILL_COLOR, "#247DCACC"); -export const TRAINING_AREAS_AOI_OUTLINE_COLOR: string = parseStringEnv(ENVS.TRAINING_AREAS_AOI_OUTLINE_COLOR, "#247DCACC"); -export const TRAINING_AREAS_AOI_OUTLINE_WIDTH: number = parseIntEnv(ENVS.TRAINING_AREAS_AOI_OUTLINE_WIDTH, 4); -export const TRAINING_AREAS_AOI_FILL_OPACITY: number = parseFloatEnv(ENVS.TRAINING_AREAS_AOI_FILL_OPACITY, 0.4); -export const TRAINING_AREAS_AOI_LABELS_FILL_OPACITY: number = parseFloatEnv(ENVS.TRAINING_AREAS_AOI_LABELS_FILL_OPACITY, 0.3); -export const TRAINING_AREAS_AOI_LABELS_OUTLINE_WIDTH: number = parseIntEnv(ENVS.TRAINING_AREAS_AOI_LABELS_OUTLINE_WIDTH, 2); -export const TRAINING_AREAS_AOI_LABELS_FILL_COLOR: string = parseStringEnv(ENVS.TRAINING_AREAS_AOI_LABELS_FILL_COLOR, "#D73434"); -export const TRAINING_AREAS_AOI_LABELS_OUTLINE_COLOR: string = parseStringEnv(ENVS.TRAINING_AREAS_AOI_LABELS_OUTLINE_COLOR, "#D73434"); +export const TRAINING_AREAS_AOI_FILL_COLOR: string = parseStringEnv( + ENVS.TRAINING_AREAS_AOI_FILL_COLOR, + "#247DCACC", +); +export const TRAINING_AREAS_AOI_OUTLINE_COLOR: string = parseStringEnv( + ENVS.TRAINING_AREAS_AOI_OUTLINE_COLOR, + "#247DCACC", +); +export const TRAINING_AREAS_AOI_OUTLINE_WIDTH: number = parseIntEnv( + ENVS.TRAINING_AREAS_AOI_OUTLINE_WIDTH, + 4, +); +export const TRAINING_AREAS_AOI_FILL_OPACITY: number = parseFloatEnv( + ENVS.TRAINING_AREAS_AOI_FILL_OPACITY, + 0.4, +); +export const TRAINING_AREAS_AOI_LABELS_FILL_OPACITY: number = parseFloatEnv( + ENVS.TRAINING_AREAS_AOI_LABELS_FILL_OPACITY, + 0.3, +); +export const TRAINING_AREAS_AOI_LABELS_OUTLINE_WIDTH: number = parseIntEnv( + ENVS.TRAINING_AREAS_AOI_LABELS_OUTLINE_WIDTH, + 2, +); +export const TRAINING_AREAS_AOI_LABELS_FILL_COLOR: string = parseStringEnv( + ENVS.TRAINING_AREAS_AOI_LABELS_FILL_COLOR, + "#D73434", +); +export const TRAINING_AREAS_AOI_LABELS_OUTLINE_COLOR: string = parseStringEnv( + ENVS.TRAINING_AREAS_AOI_LABELS_OUTLINE_COLOR, + "#D73434", +); // Start Mapping Legend - only the fill layers are in the legend. export const LEGEND_NAME_MAPPING: Record = { - [ALL_MODEL_PREDICTIONS_FILL_LAYER_ID]: "Map Result", - [REJECTED_MODEL_PREDICTIONS_FILL_LAYER_ID]: "Rejected", - [ACCEPTED_MODEL_PREDICTIONS_FILL_LAYER_ID]: "Accepted", + [ALL_MODEL_PREDICTIONS_FILL_LAYER_ID]: "Map Result", + [REJECTED_MODEL_PREDICTIONS_FILL_LAYER_ID]: "Rejected", + [ACCEPTED_MODEL_PREDICTIONS_FILL_LAYER_ID]: "Accepted", }; - - // ============================================================================================================================== // Others // ============================================================================================================================== - /** * The web component tag name used in `hotosm/ui` for the tracking component. */ @@ -254,29 +315,37 @@ export const MATOMO_ID: string = parseStringEnv(ENVS.MATOMO_ID, "0"); /** * The matomo application domain. */ -export const MATOMO_APP_DOMAIN: string = parseStringEnv(ENVS.MATOMO_APP_DOMAIN, "fair.hotosm.org"); +export const MATOMO_APP_DOMAIN: string = parseStringEnv( + ENVS.MATOMO_APP_DOMAIN, + "fair.hotosm.org", +); /** * The file extensions for the prediction api. */ export const PREDICTION_API_FILE_EXTENSIONS: Record = { - [BASE_MODELS.RAMP]: ".tflite", - [BASE_MODELS.YOLOV8_V1]: ".onnx", - [BASE_MODELS.YOLOV8_V2]: ".onnx", + [BASE_MODELS.RAMP]: ".tflite", + [BASE_MODELS.YOLOV8_V1]: ".onnx", + [BASE_MODELS.YOLOV8_V2]: ".onnx", }; /** * The time to poll the backend for the status of the AOI training labels fetching, in milliseconds (ms). * Default value: 5000 ms (5 seconds). */ -export const TRAINING_AREA_LABELS_FETCH_POOLING_TIME_MS: number = parseIntEnv(ENVS.TRAINING_AREA_LABELS_FETCH_POOLING_INTERVAL_MS, 5000); +export const TRAINING_AREA_LABELS_FETCH_POOLING_TIME_MS: number = parseIntEnv( + ENVS.TRAINING_AREA_LABELS_FETCH_POOLING_INTERVAL_MS, + 5000, +); /** * The time to poll the backend for the status of the OSM last updated time, in milliseconds (ms). * Default value: 10000 (ms i.e 10 seconds). */ -export const OSM_LAST_UPDATED_POOLING_INTERVAL_MS: number = parseIntEnv(ENVS.OSM_LAST_UPDATED_POOLING_INTERVAL_MS, 10000); - +export const OSM_LAST_UPDATED_POOLING_INTERVAL_MS: number = parseIntEnv( + ENVS.OSM_LAST_UPDATED_POOLING_INTERVAL_MS, + 10000, +); /** * The current version of the application. @@ -288,7 +357,10 @@ export const FAIR_VERSION: string = parseStringEnv(ENVS.FAIR_VERSION, "v0.1"); * Comma separated hashtags to add to the OSM ID Editor redirection. * This is used in the OSM redirect callback when a training area is opened in OSM. */ -export const OSM_HASHTAGS: string = parseStringEnv(ENVS.OSM_HASHTAGS, "#HOT-fAIr"); +export const OSM_HASHTAGS: string = parseStringEnv( + ENVS.OSM_HASHTAGS, + "#HOT-fAIr", +); /** * Configuration for KPI Statistics Refetching Interval. @@ -304,16 +376,15 @@ const REFRESH_BUFFER_MS: number = 1000; * The cache time to poll the backend for updated KPI statistics, in milliseconds. * It includes an additional buffer to ensure fresh data retrieval. */ -export const KPI_STATS_CACHE_TIME_MS: number = parseIntEnv(ENVS.KPI_STATS_CACHE_TIME, DEFAULT_KPI_STATS_CACHE_TIME_SECONDS) * 1000 + REFRESH_BUFFER_MS; - - +export const KPI_STATS_CACHE_TIME_MS: number = + parseIntEnv(ENVS.KPI_STATS_CACHE_TIME, DEFAULT_KPI_STATS_CACHE_TIME_SECONDS) * + 1000 + + REFRESH_BUFFER_MS; // ============================================================================================================================== // UI Settings // ============================================================================================================================== - - /** * Distance of the elements from the navbar in px for dropdowns and popups on the start mapping page. */ diff --git a/frontend/src/constants/routes.ts b/frontend/src/constants/routes.ts index b3fb71bd..f32336e6 100644 --- a/frontend/src/constants/routes.ts +++ b/frontend/src/constants/routes.ts @@ -44,8 +44,7 @@ export const APPLICATION_ROUTES = { START_MAPPING_BASE: "/start-mapping/", START_MAPPING: "/start-mapping/:modelId", NOTFOUND: "/404", - LOGIN: "/login", - LOGIN: "/404", + AUTH_CALLBACK: "/authenticate", PRIVACY_POLICY: "/privacy", LEARN: "/learn", ABOUT: "/about", diff --git a/frontend/src/constants/ui-contents/auth-content.ts b/frontend/src/constants/ui-contents/auth-content.ts new file mode 100644 index 00000000..651c2b47 --- /dev/null +++ b/frontend/src/constants/ui-contents/auth-content.ts @@ -0,0 +1,10 @@ +import { TAuthPageAndModalContent } from "@/types"; + +export const AUTH_PAGE_AND_MODAL_CONTENT: TAuthPageAndModalContent = { + pageTitle: "Authenticating...", + title: "Welcome to fAIr", + instruction: + "Sign In / Sign Up to fAIr using your OpenStreetMap (OSM) account.", + buttonText: "Sign In/Sign Up with OSM", + authInProgressText: "Signing you in...", +}; diff --git a/frontend/src/constants/ui-contents/map-content.ts b/frontend/src/constants/ui-contents/map-content.ts index e39fc737..cdd2d5ce 100644 --- a/frontend/src/constants/ui-contents/map-content.ts +++ b/frontend/src/constants/ui-contents/map-content.ts @@ -1,4 +1,4 @@ -import { TMapContent } from '@/types'; +import { TMapContent } from "@/types"; export const MAP_CONTENT: TMapContent = { controls: { diff --git a/frontend/src/constants/ui-contents/models-content.ts b/frontend/src/constants/ui-contents/models-content.ts index b8f8d803..47ff69c1 100644 --- a/frontend/src/constants/ui-contents/models-content.ts +++ b/frontend/src/constants/ui-contents/models-content.ts @@ -1,8 +1,7 @@ -import { BASE_MODELS } from '@/enums'; -import { formatAreaInAppropriateUnit } from '@/utils'; -import { MAX_TRAINING_AREA_SIZE, MIN_TRAINING_AREA_SIZE } from '@/config'; -import { TModelsContent } from '@/types'; - +import { BASE_MODELS } from "@/enums"; +import { formatAreaInAppropriateUnit } from "@/utils"; +import { MAX_TRAINING_AREA_SIZE, MIN_TRAINING_AREA_SIZE } from "@/config"; +import { TModelsContent } from "@/types"; export const MODELS_CONTENT: TModelsContent = { trainingArea: { @@ -32,11 +31,15 @@ export const MODELS_CONTENT: TModelsContent = { }, baseModel: { label: "Base Model", - helpText: "Choose a base model to use for training. All base models currently support building detection.", - toolTip: "A base model is the pre-trained model that serves as the foundation for fine-tuning your local AI model.", + helpText: + "Choose a base model to use for training. All base models currently support building detection.", + toolTip: + "A base model is the pre-trained model that serves as the foundation for fine-tuning your local AI model.", suffixes: { - [BASE_MODELS.RAMP]: "Optimized for faster training with decent accuracy. Best suited for building detection tasks.", - [BASE_MODELS.YOLOV8_V1]: "A well-balanced model offering good accuracy for detecting structures in major areas. Trained by the community.", + [BASE_MODELS.RAMP]: + "Optimized for faster training with decent accuracy. Best suited for building detection tasks.", + [BASE_MODELS.YOLOV8_V1]: + "A well-balanced model offering good accuracy for detecting structures in major areas. Trained by the community.", [BASE_MODELS.YOLOV8_V2]: "Our most advanced model. Designed for detecting various features across different areas. Developed in collaboration with Omdena AI.", }, @@ -68,13 +71,15 @@ export const MODELS_CONTENT: TModelsContent = { }, tmsURL: { label: "TMS URL", - toolTip: "Enter the Tile Map Service (TMS) URL. You can input the TMS from OpenAerialMap (OAM), or provide a custom one.", + toolTip: + "Enter the Tile Map Service (TMS) URL. You can input the TMS from OpenAerialMap (OAM), or provide a custom one.", helpText: "TMS imagery link should look like this https://tiles.openaerialmap.org/****/*/***/{z}/{x}/{y}", placeholder: "https://tiles.openaerialmap.org/****/*/***/{z}/{x}/{y}", }, existingTrainingDatasetSectionHeading: "Existing Training Dataset", - existingTrainingDatasetSectionDescription: 'Browse or search for a dataset name. Select a dataset to proceed.', + existingTrainingDatasetSectionDescription: + "Browse or search for a dataset name. Select a dataset to proceed.", newTrainingDatasetSectionHeading: "Create New Training Dataset", searchBar: { placeholder: "Enter a dataset name to search", @@ -88,16 +93,19 @@ export const MODELS_CONTENT: TModelsContent = { trainingArea: { toolTips: { labelsFetchInProgress: "Processing labels...", - fetchOSMLabels: "Click to retrieve mapped buildings from OpenStreetMap (OSM) for this area. These buildings will be used as training labels to help the model learn.", + fetchOSMLabels: + "Click to retrieve mapped buildings from OpenStreetMap (OSM) for this area. These buildings will be used as training labels to help the model learn.", lastUpdatedPrefix: "OSM last synced:", zoomToAOI: "Click to zoom to this training area.", openINJOSM: "Click to open this training area in JOSM.", openInIdEditor: "Click to open this training area in ID Editor.", downloadAOI: "Click to download this training area as GeoJSON.", - downloadLabels: "Click to download the labels in this training area as GeoJSON.", + downloadLabels: + "Click to download the labels in this training area as GeoJSON.", uploadLabels: "Click to upload training labels for this training area.", deleteAOI: "Click to delete this training area.", - fitToTMSBounds: "Click to adjust the map view to fit the imagery bounds.", + fitToTMSBounds: + "Click to adjust the map view to fit the imagery bounds.", }, pageTitle: "Create Training Area", datasetID: "Dataset ID:", @@ -122,7 +130,6 @@ export const MODELS_CONTENT: TModelsContent = { "Drag 'n' drop some files here, or click to select files", fleSizeInstruction: "Supports only GeoJSON (.geojson) files. (5MB max.)", - aoiAreaInstruction: `Area should be > ${formatAreaInAppropriateUnit(MIN_TRAINING_AREA_SIZE)} and < ${formatAreaInAppropriateUnit(MAX_TRAINING_AREA_SIZE)}.`, }, pageDescription: "Make sure you create at least one training area and data is accurate for each training area", @@ -139,8 +146,7 @@ export const MODELS_CONTENT: TModelsContent = { zoomLevels: "Zoom Levels", trainingSettings: "Training Settings", }, - pageDescription: - "Please check all the model details before you proceed!", + pageDescription: "Please check all the model details before you proceed!", }, confirmation: { buttons: { @@ -155,7 +161,8 @@ export const MODELS_CONTENT: TModelsContent = { form: { zoomLevel: { label: "Select Zoom Level", - toolTip: "Choose the zoom level for training. A higher zoom level provides finer details but may increase training time.", + toolTip: + "Choose the zoom level for training. A higher zoom level provides finer details but may increase training time.", }, trainingType: { label: "Select Model Training Type", @@ -163,27 +170,33 @@ export const MODELS_CONTENT: TModelsContent = { }, advancedSettings: { label: "Advanced Settings", - toolTip: "Modify additional parameters for fine-tuning your model training.", + toolTip: + "Modify additional parameters for fine-tuning your model training.", }, epoch: { label: "Epoch", - toolTip: "Specify the number of training iterations. A higher number improves learning.", + toolTip: + "Specify the number of training iterations. A higher number improves learning.", }, contactSpacing: { label: "Contact Spacing", - toolTip: "Defines the minimum spacing between detected objects during training.", + toolTip: + "Defines the minimum spacing between detected objects during training.", }, batchSize: { label: "Batch Size", - toolTip: "The number of training samples processed in one step. A larger batch size may speed up training.", + toolTip: + "The number of training samples processed in one step. A larger batch size may speed up training.", }, boundaryWidth: { label: "Boundary Width", - toolTip: "Determines the width of the boundary around detected objects, affecting how edges are handled.", + toolTip: + "Determines the width of the boundary around detected objects, affecting how edges are handled.", }, }, pageTitle: "Model Training Settings", - pageDescription: "Customize your model training preferences by selecting the appropriate options below.", + pageDescription: + "Customize your model training preferences by selecting the appropriate options below.", }, progressStepper: { modelDetails: "Model Details", @@ -321,8 +334,7 @@ export const MODELS_CONTENT: TModelsContent = { }, trainingSettings: { dialogHeading: "Model Training Settings", - description: - "Please make sure the following settings are accurate!", + description: "Please make sure the following settings are accurate!", submitButtonText: "Submit", }, modelEnhancement: { diff --git a/frontend/src/constants/ui-contents/shared-content.ts b/frontend/src/constants/ui-contents/shared-content.ts index 56997571..bd9a403c 100644 --- a/frontend/src/constants/ui-contents/shared-content.ts +++ b/frontend/src/constants/ui-contents/shared-content.ts @@ -1,5 +1,5 @@ -import { APPLICATION_ROUTES } from '../routes'; -import { TSharedContent } from '@/types'; +import { APPLICATION_ROUTES } from "../routes"; +import { TSharedContent } from "@/types"; export const SHARED_CONTENT: TSharedContent = { navbar: { diff --git a/frontend/src/constants/ui-contents/start-mapping-content.ts b/frontend/src/constants/ui-contents/start-mapping-content.ts index 4ffce908..a7e95236 100644 --- a/frontend/src/constants/ui-contents/start-mapping-content.ts +++ b/frontend/src/constants/ui-contents/start-mapping-content.ts @@ -1,4 +1,4 @@ -import { TStartMappingPageContent } from '@/types'; +import { TStartMappingPageContent } from "@/types"; export const START_MAPPING_PAGE_CONTENT: TStartMappingPageContent = { pageTitle: (modelName: string) => `Start Mapping with ${modelName}`, diff --git a/frontend/src/features/model-creation/components/dialogs/file-upload-dialog.tsx b/frontend/src/features/model-creation/components/dialogs/file-upload-dialog.tsx index d63b83fd..b22bda6e 100644 --- a/frontend/src/features/model-creation/components/dialogs/file-upload-dialog.tsx +++ b/frontend/src/features/model-creation/components/dialogs/file-upload-dialog.tsx @@ -1,18 +1,20 @@ -import { Button } from '@/components/ui/button'; -import { DeleteIcon, FileIcon, UploadIcon } from '@/components/ui/icons'; -import { Dialog } from '@/components/ui/dialog'; -import { DialogProps, Feature, FeatureCollection } from '@/types'; -import { FileWithPath, useDropzone } from 'react-dropzone'; -import { Geometry, MultiPolygon, Polygon } from 'geojson'; -import { MODELS_CONTENT } from '@/constants'; -import { SlFormatBytes } from '@shoelace-style/shoelace/dist/react'; -import { Spinner } from '@/components/ui/spinner'; -import { useCallback, useState } from 'react'; +import { Button } from "@/components/ui/button"; +import { DeleteIcon, FileIcon, UploadIcon } from "@/components/ui/icons"; +import { Dialog } from "@/components/ui/dialog"; +import { DialogProps, Feature, FeatureCollection } from "@/types"; +import { FileWithPath, useDropzone } from "react-dropzone"; +import { Geometry, MultiPolygon, Polygon } from "geojson"; +import { MODELS_CONTENT } from "@/constants"; +import { SlFormatBytes } from "@shoelace-style/shoelace/dist/react"; +import { Spinner } from "@/components/ui/spinner"; +import { useCallback, useState } from "react"; import { MAX_ACCEPTABLE_POLYGON_IN_TRAINING_AREA_GEOJSON_FILE, MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREA_LABELS, MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREAS, + MAX_TRAINING_AREA_SIZE, MAX_TRAINING_AREA_UPLOAD_FILE_SIZE, + MIN_TRAINING_AREA_SIZE, } from "@/config"; import { @@ -141,7 +143,7 @@ const FileUploadDialog: React.FC = ({ disabled || uploadInProgress || acceptedFiles.length === - MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREA_LABELS || + MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREA_LABELS || acceptedFiles.length === MAX_GEOJSON_FILE_UPLOAD_FOR_TRAINING_AREAS, }); @@ -307,10 +309,7 @@ const FileUploadDialog: React.FC = ({ {!disableFileSizeValidation && ( - { - MODELS_CONTENT.modelCreation.trainingArea.fileUploadDialog - .aoiAreaInstruction - } + {`Area should be > ${formatAreaInAppropriateUnit(MIN_TRAINING_AREA_SIZE)} and < ${formatAreaInAppropriateUnit(MAX_TRAINING_AREA_SIZE)}.`} )} diff --git a/frontend/src/features/model-creation/components/model-details/model-description-input.tsx b/frontend/src/features/model-creation/components/model-details/model-description-input.tsx index ac8ad630..66e2543a 100644 --- a/frontend/src/features/model-creation/components/model-details/model-description-input.tsx +++ b/frontend/src/features/model-creation/components/model-details/model-description-input.tsx @@ -1,5 +1,5 @@ -import { MODELS_CONTENT } from '@/constants'; -import { TextArea } from '@/components/ui/form'; +import { MODELS_CONTENT } from "@/constants"; +import { TextArea } from "@/components/ui/form"; import { FORM_VALIDATION_CONFIG, MODEL_CREATION_FORM_NAME, diff --git a/frontend/src/features/model-creation/components/model-details/model-details.tsx b/frontend/src/features/model-creation/components/model-details/model-details.tsx index 6a33b84e..72f0c1f7 100644 --- a/frontend/src/features/model-creation/components/model-details/model-details.tsx +++ b/frontend/src/features/model-creation/components/model-details/model-details.tsx @@ -1,9 +1,9 @@ -import ModelDescriptionFormInput from './model-description-input'; -import ModelNameFormInput from '@/features/model-creation/components/model-details/model-name-input'; -import { BASE_MODELS } from '@/enums'; -import { MODELS_CONTENT } from '@/constants'; -import { Select } from '@/components/ui/form'; -import { StepHeading } from '@/features/model-creation/components/'; +import ModelDescriptionFormInput from "./model-description-input"; +import ModelNameFormInput from "@/features/model-creation/components/model-details/model-name-input"; +import { BASE_MODELS } from "@/enums"; +import { MODELS_CONTENT } from "@/constants"; +import { Select } from "@/components/ui/form"; +import { StepHeading } from "@/features/model-creation/components/"; import { MODEL_CREATION_FORM_NAME, useModelsContext, @@ -15,7 +15,7 @@ const baseModelOptions = [ value: BASE_MODELS.RAMP, suffix: MODELS_CONTENT.modelCreation.modelDetails.form.baseModel.suffixes[ - BASE_MODELS.RAMP + BASE_MODELS.RAMP ], }, { @@ -23,7 +23,7 @@ const baseModelOptions = [ value: BASE_MODELS.YOLOV8_V1, suffix: MODELS_CONTENT.modelCreation.modelDetails.form.baseModel.suffixes[ - BASE_MODELS.YOLOV8_V1 + BASE_MODELS.YOLOV8_V1 ], }, { @@ -31,7 +31,7 @@ const baseModelOptions = [ value: BASE_MODELS.YOLOV8_V2, suffix: MODELS_CONTENT.modelCreation.modelDetails.form.baseModel.suffixes[ - BASE_MODELS.YOLOV8_V2 + BASE_MODELS.YOLOV8_V2 ], }, ]; @@ -71,7 +71,6 @@ const ModelDetailsForm = () => { handleChange={(value) => handleChange(MODEL_CREATION_FORM_NAME.BASE_MODELS, value) } - /> diff --git a/frontend/src/features/model-creation/components/model-details/model-name-input.tsx b/frontend/src/features/model-creation/components/model-details/model-name-input.tsx index 73b35a7a..40dd8039 100644 --- a/frontend/src/features/model-creation/components/model-details/model-name-input.tsx +++ b/frontend/src/features/model-creation/components/model-details/model-name-input.tsx @@ -1,5 +1,5 @@ -import { Input } from '@/components/ui/form'; -import { MODELS_CONTENT } from '@/constants'; +import { Input } from "@/components/ui/form"; +import { MODELS_CONTENT } from "@/constants"; import { FORM_VALIDATION_CONFIG, MODEL_CREATION_FORM_NAME, diff --git a/frontend/src/features/model-creation/components/model-summary.tsx b/frontend/src/features/model-creation/components/model-summary.tsx index e0411086..35200cdd 100644 --- a/frontend/src/features/model-creation/components/model-summary.tsx +++ b/frontend/src/features/model-creation/components/model-summary.tsx @@ -1,8 +1,8 @@ -import { BASE_MODELS } from '@/enums'; -import { IconProps } from '@/types'; -import { MODELS_CONTENT } from '@/constants'; -import { StepHeading } from '@/features/model-creation/components/'; -import { useModelsContext } from '@/app/providers/models-provider'; +import { BASE_MODELS } from "@/enums"; +import { IconProps } from "@/types"; +import { MODELS_CONTENT } from "@/constants"; +import { StepHeading } from "@/features/model-creation/components/"; +import { useModelsContext } from "@/app/providers/models-provider"; import { DatabaseIcon, MapIcon, @@ -91,9 +91,7 @@ const ModelSummaryForm = () => {
{summaryData.map((item, index) => ( = ({ pages, }) => { const navigate = useNavigate(); - const { getFullPath, } = useModelsContext(); + const { getFullPath } = useModelsContext(); const activeStepRef = useRef(null); const containerRef = useRef(null); @@ -50,9 +50,7 @@ const ProgressBar: React.FC = ({ ref={activeStep ? activeStepRef : null} className="flex items-center gap-x-3 cursor-pointer" disabled={isLastPage} - onClick={() => - !isLastPage && navigate(getFullPath(step.path)) - } + onClick={() => !isLastPage && navigate(getFullPath(step.path))} > {step.id < currentPageIndex + 1 ? ( @@ -61,9 +59,10 @@ const ProgressBar: React.FC = ({ ) : ( diff --git a/frontend/src/features/model-creation/components/training-area/open-area-map.tsx b/frontend/src/features/model-creation/components/training-area/open-area-map.tsx index 2bbd9bce..e9df688a 100644 --- a/frontend/src/features/model-creation/components/training-area/open-area-map.tsx +++ b/frontend/src/features/model-creation/components/training-area/open-area-map.tsx @@ -1,11 +1,11 @@ -import { FullScreenIcon } from '@/components/ui/icons'; -import { Map } from 'maplibre-gl'; -import { MODELS_CONTENT } from '@/constants'; -import { showErrorToast } from '@/utils'; -import { ToolTip } from '@/components/ui/tooltip'; -import { useCallback, useEffect } from 'react'; -import { useGetTMSTileJSON } from '@/features/model-creation/hooks/use-tms-tilejson'; -import { useGetTrainingDataset } from '@/features/models/hooks/use-dataset'; +import { FullScreenIcon } from "@/components/ui/icons"; +import { Map } from "maplibre-gl"; +import { MODELS_CONTENT } from "@/constants"; +import { showErrorToast } from "@/utils"; +import { ToolTip } from "@/components/ui/tooltip"; +import { useCallback, useEffect } from "react"; +import { useGetTMSTileJSON } from "@/features/model-creation/hooks/use-tms-tilejson"; +import { useGetTrainingDataset } from "@/features/models/hooks/use-dataset"; import { MODEL_CREATION_FORM_NAME, useModelsContext, @@ -29,7 +29,7 @@ const OpenAerialMap = ({ useEffect(() => { if (trainingDatasetFetchError) { - showErrorToast(undefined, 'Failed to fetch training dataset'); + showErrorToast(undefined, "Failed to fetch training dataset"); } }, [trainingDatasetFetchError]); diff --git a/frontend/src/features/model-creation/components/training-area/training-area-item.tsx b/frontend/src/features/model-creation/components/training-area/training-area-item.tsx index eb9ab866..777ac68c 100644 --- a/frontend/src/features/model-creation/components/training-area/training-area-item.tsx +++ b/frontend/src/features/model-creation/components/training-area/training-area-item.tsx @@ -1,20 +1,15 @@ -import FileUploadDialog from '@/features/model-creation/components/dialogs/file-upload-dialog'; -import { DropDown } from '@/components/ui/dropdown'; -import { IconProps, TTrainingAreaFeature } from '@/types'; -import { JOSMLogo, OSMLogo } from '@/assets/svgs'; -import { LabelStatus } from '@/enums/training-area'; -import { Map } from 'maplibre-gl'; -import { ToolTip } from '@/components/ui/tooltip'; -import { TRAINING_AREA_LABELS_FETCH_POOLING_TIME_MS } from '@/config'; -import { - useCallback, - useEffect, - useRef, - useState - } from 'react'; -import { useDialog } from '@/hooks/use-dialog'; -import { useDropdownMenu } from '@/hooks/use-dropdown-menu'; -import { useModelsContext } from '@/app/providers/models-provider'; +import FileUploadDialog from "@/features/model-creation/components/dialogs/file-upload-dialog"; +import { DropDown } from "@/components/ui/dropdown"; +import { IconProps, TTrainingAreaFeature } from "@/types"; +import { JOSMLogo, OSMLogo } from "@/assets/svgs"; +import { LabelStatus } from "@/enums/training-area"; +import { Map } from "maplibre-gl"; +import { ToolTip } from "@/components/ui/tooltip"; +import { TRAINING_AREA_LABELS_FETCH_POOLING_TIME_MS } from "@/config"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { useDialog } from "@/hooks/use-dialog"; +import { useDropdownMenu } from "@/hooks/use-dropdown-menu"; +import { useModelsContext } from "@/app/providers/models-provider"; import { CloudDownloadIcon, DeleteIcon, @@ -36,10 +31,7 @@ import { showWarningToast, truncateString, } from "@/utils"; -import { - MODELS_CONTENT, - TOAST_NOTIFICATIONS, -} from "@/constants"; +import { MODELS_CONTENT, TOAST_NOTIFICATIONS } from "@/constants"; import { useCreateTrainingLabelsForAOI, useDeleteTrainingArea, @@ -102,7 +94,9 @@ const LabelFetchStatus = ({ if (isFetching) return "Fetching labels..."; if (isError) return "Error occurred. Please retry."; if (status === LabelStatus.DOWNLOADED) { - return timeSince ? `Labels fetched ${timeSince} ago` : "Labels fetched recently"; + return timeSince + ? `Labels fetched ${timeSince} ago` + : "Labels fetched recently"; } return "No labels yet"; }; @@ -294,7 +288,7 @@ export const TrainingAreaItem: React.FC< shouldPoll: false, errorToastShown: false, })); - refetchTrainingAreas() + refetchTrainingAreas(); showSuccessToast( `Training labels for Training Area ${trainingArea.id} have been successfully fetched.`, ); @@ -422,7 +416,7 @@ export const TrainingAreaItem: React.FC< { tooltip: disableLabelsFetchOrUpload ? MODELS_CONTENT.modelCreation.trainingArea.toolTips - .labelsFetchInProgress + .labelsFetchInProgress : MODELS_CONTENT.modelCreation.trainingArea.toolTips.uploadLabels, isIcon: true, Icon: UploadIcon, @@ -477,9 +471,9 @@ export const TrainingAreaItem: React.FC< content={ disableLabelsFetchOrUpload ? MODELS_CONTENT.modelCreation.trainingArea.toolTips - .labelsFetchInProgress + .labelsFetchInProgress : MODELS_CONTENT.modelCreation.trainingArea.toolTips - .fetchOSMLabels + .fetchOSMLabels } >
- +
@@ -123,7 +127,11 @@ const TrainingAreaForm = () => { />
- + { validationState, ) } - isValid={formData.tmsURLValidation.valid} />
diff --git a/frontend/src/features/model-creation/components/training-dataset/select-existing.tsx b/frontend/src/features/model-creation/components/training-dataset/select-existing.tsx index 824a7928..135c33ed 100644 --- a/frontend/src/features/model-creation/components/training-dataset/select-existing.tsx +++ b/frontend/src/features/model-creation/components/training-dataset/select-existing.tsx @@ -1,11 +1,11 @@ -import useDebounce from '@/hooks/use-debounce'; -import { CheckIcon } from '@/components/ui/icons'; -import { HelpText, Input } from '@/components/ui/form'; -import { MODELS_CONTENT } from '@/constants'; -import { SearchIcon } from '@/components/ui/icons'; -import { SkeletonWrapper } from '@/components/ui/skeleton'; -import { useGetTrainingDatasets } from '@/features/model-creation/hooks/use-training-datasets'; -import { useState } from 'react'; +import useDebounce from "@/hooks/use-debounce"; +import { CheckIcon } from "@/components/ui/icons"; +import { HelpText, Input } from "@/components/ui/form"; +import { MODELS_CONTENT } from "@/constants"; +import { SearchIcon } from "@/components/ui/icons"; +import { SkeletonWrapper } from "@/components/ui/skeleton"; +import { useGetTrainingDatasets } from "@/features/model-creation/hooks/use-training-datasets"; +import { useState } from "react"; import { MODEL_CREATION_FORM_NAME, useModelsContext, @@ -27,8 +27,12 @@ const SelectExistingTrainingDatasetForm = () => { .existingTrainingDatasetSectionHeading }

- +
@@ -43,7 +47,6 @@ const SelectExistingTrainingDatasetForm = () => { } disabled={isError} className="w-full" - />
@@ -69,7 +72,6 @@ const SelectExistingTrainingDatasetForm = () => { disabled={!td.source_imagery} className="w-full text-start" onClick={() => { - handleChange( MODEL_CREATION_FORM_NAME.SELECTED_TRAINING_DATASET_ID, String(td.id), diff --git a/frontend/src/features/model-creation/components/training-dataset/training-dataset.tsx b/frontend/src/features/model-creation/components/training-dataset/training-dataset.tsx index 069361eb..2487f0b4 100644 --- a/frontend/src/features/model-creation/components/training-dataset/training-dataset.tsx +++ b/frontend/src/features/model-creation/components/training-dataset/training-dataset.tsx @@ -1,10 +1,10 @@ -import CreateNewTrainingDatasetForm from '@/features/model-creation/components/training-dataset/create-new'; -import SelectExistingTrainingDatasetForm from '@/features/model-creation/components/training-dataset/select-existing'; -import { ButtonWithIcon } from '@/components/ui/button'; -import { ChevronDownIcon } from '@/components/ui/icons'; -import { MODELS_CONTENT } from '@/constants'; -import { StepHeading } from '@/features/model-creation/components/'; -import { TrainingDatasetOption } from '@/enums'; +import CreateNewTrainingDatasetForm from "@/features/model-creation/components/training-dataset/create-new"; +import SelectExistingTrainingDatasetForm from "@/features/model-creation/components/training-dataset/select-existing"; +import { ButtonWithIcon } from "@/components/ui/button"; +import { ChevronDownIcon } from "@/components/ui/icons"; +import { MODELS_CONTENT } from "@/constants"; +import { StepHeading } from "@/features/model-creation/components/"; +import { TrainingDatasetOption } from "@/enums"; import { MODEL_CREATION_FORM_NAME, useModelsContext, diff --git a/frontend/src/features/model-creation/hooks/use-tms-tilejson.ts b/frontend/src/features/model-creation/hooks/use-tms-tilejson.ts index e4449200..9378e1f2 100644 --- a/frontend/src/features/model-creation/hooks/use-tms-tilejson.ts +++ b/frontend/src/features/model-creation/hooks/use-tms-tilejson.ts @@ -1,6 +1,5 @@ -import { getTMSTileJSONQueryOptions } from '@/features/model-creation/api/factory'; -import { useQuery } from '@tanstack/react-query'; - +import { getTMSTileJSONQueryOptions } from "@/features/model-creation/api/factory"; +import { useQuery } from "@tanstack/react-query"; export const useGetTMSTileJSON = (url: string) => { return useQuery({ diff --git a/frontend/src/features/model-creation/hooks/use-training-areas.ts b/frontend/src/features/model-creation/hooks/use-training-areas.ts index 5b23641f..58b17198 100644 --- a/frontend/src/features/model-creation/hooks/use-training-areas.ts +++ b/frontend/src/features/model-creation/hooks/use-training-areas.ts @@ -1,8 +1,8 @@ -import axios from 'axios'; -import { API_ENDPOINTS, MutationConfig } from '@/services'; -import { deleteTrainingArea } from '@/features/model-creation/api/delete-trainings'; -import { MIN_ZOOM_LEVEL_FOR_TRAINING_AREA_LABELS } from '@/config'; -import { useMutation, useQuery } from '@tanstack/react-query'; +import axios from "axios"; +import { API_ENDPOINTS, MutationConfig } from "@/services"; +import { deleteTrainingArea } from "@/features/model-creation/api/delete-trainings"; +import { MIN_ZOOM_LEVEL_FOR_TRAINING_AREA_LABELS } from "@/config"; +import { useMutation, useQuery } from "@tanstack/react-query"; import { getTrainingAreaLabelsQueryOptions, getTrainingAreaQueryOptions, @@ -108,7 +108,6 @@ type useGetTrainingAreaLabelsFromOSMOptions = { export const useGetTrainingAreaLabelsFromOSM = ({ mutationConfig, }: useGetTrainingAreaLabelsFromOSMOptions) => { - const { onSuccess, ...restConfig } = mutationConfig || {}; return useMutation({ diff --git a/frontend/src/features/models/components/accuracy-display.tsx b/frontend/src/features/models/components/accuracy-display.tsx index b3eea341..ab20ef63 100644 --- a/frontend/src/features/models/components/accuracy-display.tsx +++ b/frontend/src/features/models/components/accuracy-display.tsx @@ -1,4 +1,4 @@ -import { roundNumber } from '@/utils'; +import { roundNumber } from "@/utils"; const AccuracyDisplay = ({ accuracy }: { accuracy: number }) => { const colors = [ diff --git a/frontend/src/features/models/components/maps/training-area-map.tsx b/frontend/src/features/models/components/maps/training-area-map.tsx index bf078108..8d799bf1 100644 --- a/frontend/src/features/models/components/maps/training-area-map.tsx +++ b/frontend/src/features/models/components/maps/training-area-map.tsx @@ -1,14 +1,9 @@ -import { ControlsPosition } from '@/enums'; -import { errorMessages } from '@/constants'; -import { MapComponent } from '@/components/map'; -import { PMTiles } from 'pmtiles'; -import { - useCallback, - useEffect, - useRef, - useState - } from 'react'; -import { useMapInstance } from '@/hooks/use-map-instance'; +import { ControlsPosition } from "@/enums"; +import { errorMessages } from "@/constants"; +import { MapComponent } from "@/components/map"; +import { PMTiles } from "pmtiles"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { useMapInstance } from "@/hooks/use-map-instance"; import { LayerSpecification, MapLayerMouseEvent, @@ -31,7 +26,6 @@ import { TRAINING_AREAS_AOI_LABELS_OUTLINE_WIDTH, TRAINING_AREAS_AOI_OUTLINE_COLOR, TRAINING_AREAS_AOI_OUTLINE_WIDTH, - } from "@/config"; type Metadata = { @@ -178,15 +172,15 @@ export const TrainingAreaMap = ({ ${Object.entries(feature.properties) - .map( - ([key, value]) => ` + .map( + ([key, value]) => ` `, - ) - .join("")} + ) + .join("")}
${key} ${typeof value === "boolean" ? JSON.stringify(value) : value}
diff --git a/frontend/src/features/models/components/model-details-properties.tsx b/frontend/src/features/models/components/model-details-properties.tsx index bce0f3ca..a62b6f64 100644 --- a/frontend/src/features/models/components/model-details-properties.tsx +++ b/frontend/src/features/models/components/model-details-properties.tsx @@ -1,21 +1,21 @@ -import AccuracyDisplay from './accuracy-display'; -import CodeBlock from '@/components/ui/codeblock/codeblock'; -import ModelFilesButton from './model-files-button'; -import ToolTip from '@/components/ui/tooltip/tooltip'; -import useCopyToClipboard from '@/hooks/use-clipboard'; -import { BASE_API_URL } from '@/config'; -import { ChevronDownIcon } from '@/components/ui/icons'; -import { cn, showErrorToast } from '@/utils'; -import { CopyIcon, ExternalLinkIcon } from '@/components/ui/icons'; -import { Image, ZoomableImage } from '@/components/ui/image'; -import { Link } from '@/components/ui/link'; -import { ModelFilesDialog } from './dialogs'; -import { ModelPropertiesSkeleton } from './skeletons'; -import { MODELS_CONTENT } from '@/constants'; -import { TrainingAreaButton } from './training-area-button'; -import { TrainingAreaDrawer } from './training-area-drawer'; -import { useDialog } from '@/hooks/use-dialog'; -import { useEffect, useState } from 'react'; +import AccuracyDisplay from "./accuracy-display"; +import CodeBlock from "@/components/ui/codeblock/codeblock"; +import ModelFilesButton from "./model-files-button"; +import ToolTip from "@/components/ui/tooltip/tooltip"; +import useCopyToClipboard from "@/hooks/use-clipboard"; +import { BASE_API_URL } from "@/config"; +import { ChevronDownIcon } from "@/components/ui/icons"; +import { cn, showErrorToast } from "@/utils"; +import { CopyIcon, ExternalLinkIcon } from "@/components/ui/icons"; +import { Image, ZoomableImage } from "@/components/ui/image"; +import { Link } from "@/components/ui/link"; +import { ModelFilesDialog } from "./dialogs"; +import { ModelPropertiesSkeleton } from "./skeletons"; +import { MODELS_CONTENT } from "@/constants"; +import { TrainingAreaButton } from "./training-area-button"; +import { TrainingAreaDrawer } from "./training-area-drawer"; +import { useDialog } from "@/hooks/use-dialog"; +import { useEffect, useState } from "react"; import { useTrainingDetails, useTrainingStatus, diff --git a/frontend/src/features/start-mapping/api/create-feedbacks.ts b/frontend/src/features/start-mapping/api/create-feedbacks.ts index 569a2925..380db041 100644 --- a/frontend/src/features/start-mapping/api/create-feedbacks.ts +++ b/frontend/src/features/start-mapping/api/create-feedbacks.ts @@ -1,5 +1,5 @@ -import { API_ENDPOINTS, apiClient } from '@/services'; -import { Feature, TModelPredictionFeature } from '@/types'; +import { API_ENDPOINTS, apiClient } from "@/services"; +import { Feature, TModelPredictionFeature } from "@/types"; export type TCreateFeedbackPayload = { comments: string; diff --git a/frontend/src/features/start-mapping/api/get-model-predictions.ts b/frontend/src/features/start-mapping/api/get-model-predictions.ts index 012d6127..c7a7db51 100644 --- a/frontend/src/features/start-mapping/api/get-model-predictions.ts +++ b/frontend/src/features/start-mapping/api/get-model-predictions.ts @@ -1,7 +1,7 @@ -import axios from 'axios'; -import { API_ENDPOINTS } from '@/services'; -import { FeatureCollection } from 'geojson'; -import { TModelPredictionsConfig } from '@/types'; +import axios from "axios"; +import { API_ENDPOINTS } from "@/services"; +import { FeatureCollection } from "geojson"; +import { TModelPredictionsConfig } from "@/types"; export const getModelPredictions = async ({ area_threshold, diff --git a/frontend/src/features/start-mapping/components/feature-popup.tsx b/frontend/src/features/start-mapping/components/feature-popup.tsx index d77bde3a..862e0468 100644 --- a/frontend/src/features/start-mapping/components/feature-popup.tsx +++ b/frontend/src/features/start-mapping/components/feature-popup.tsx @@ -1,18 +1,12 @@ -import maplibregl, { Map, Popup } from 'maplibre-gl'; -import { CheckIcon } from '@/components/ui/icons'; -import { - Dispatch, - SetStateAction, - useEffect, - useRef, - useState -} from 'react'; -import { geojsonToWKT } from '@terraformer/wkt'; -import { Input } from '@/components/ui/form'; -import { SHOELACE_SIZES } from '@/enums'; -import { showErrorToast } from '@/utils'; -import { START_MAPPING_PAGE_CONTENT } from '@/constants'; -import { useAuth } from '@/app/providers/auth-provider'; +import maplibregl, { Map, Popup } from "maplibre-gl"; +import { CheckIcon } from "@/components/ui/icons"; +import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react"; +import { geojsonToWKT } from "@terraformer/wkt"; +import { Input } from "@/components/ui/form"; +import { SHOELACE_SIZES } from "@/enums"; +import { showErrorToast } from "@/utils"; +import { START_MAPPING_PAGE_CONTENT } from "@/constants"; +import { useAuth } from "@/app/providers/auth-provider"; import { GeoJSONType, TModelPredictionFeature, @@ -128,13 +122,13 @@ const PredictedFeatureActionPopup = ({ onSuccess: (data) => { const { updatedSource, updatedTarget } = alreadyRejected ? moveFeature(rejected, accepted, featureId, { - _id: data.id, - ...data.properties, - }) + _id: data.id, + ...data.properties, + }) : moveFeature(all, accepted, featureId, { - _id: data.id, - ...data.properties, - }); + _id: data.id, + ...data.properties, + }); setModelPredictions((prev) => ({ ...prev, @@ -177,8 +171,6 @@ const PredictedFeatureActionPopup = ({ }, }); - - const deleteApprovedModelPrediction = useDeleteApprovedModelPrediction({ mutationConfig: { onSuccess: async (_, variables) => { @@ -314,45 +306,45 @@ const PredictedFeatureActionPopup = ({ const primaryButton = alreadyAccepted ? { - label: START_MAPPING_PAGE_CONTENT.map.popup.reject, - action: handleRejection, - className: "bg-primary", - icon: RejectIcon, - } + label: START_MAPPING_PAGE_CONTENT.map.popup.reject, + action: handleRejection, + className: "bg-primary", + icon: RejectIcon, + } : alreadyRejected ? { + label: START_MAPPING_PAGE_CONTENT.map.popup.resolve, + action: handleResolve, + className: "bg-black", + icon: ResolveIcon, + } + : { + label: START_MAPPING_PAGE_CONTENT.map.popup.accept, + action: handleAcceptance, + className: "bg-green-primary", + icon: AcceptIcon, + }; + + const secondaryButton = alreadyAccepted + ? { label: START_MAPPING_PAGE_CONTENT.map.popup.resolve, action: handleResolve, className: "bg-black", icon: ResolveIcon, } - : { - label: START_MAPPING_PAGE_CONTENT.map.popup.accept, - action: handleAcceptance, - className: "bg-green-primary", - icon: AcceptIcon, - }; - - const secondaryButton = alreadyAccepted - ? { - label: START_MAPPING_PAGE_CONTENT.map.popup.resolve, - action: handleResolve, - className: "bg-black", - icon: ResolveIcon, - } : alreadyRejected ? { - label: START_MAPPING_PAGE_CONTENT.map.popup.accept, - action: handleAcceptance, - className: "bg-green-primary", - icon: AcceptIcon, - } + label: START_MAPPING_PAGE_CONTENT.map.popup.accept, + action: handleAcceptance, + className: "bg-green-primary", + icon: AcceptIcon, + } : { - label: START_MAPPING_PAGE_CONTENT.map.popup.reject, - action: handleRejection, - className: "bg-primary", - icon: RejectIcon, - }; + label: START_MAPPING_PAGE_CONTENT.map.popup.reject, + action: handleRejection, + className: "bg-primary", + icon: RejectIcon, + }; return (
diff --git a/frontend/src/features/start-mapping/components/logo-with-dropdown.tsx b/frontend/src/features/start-mapping/components/logo-with-dropdown.tsx index 463497b6..297aaffd 100644 --- a/frontend/src/features/start-mapping/components/logo-with-dropdown.tsx +++ b/frontend/src/features/start-mapping/components/logo-with-dropdown.tsx @@ -1,11 +1,11 @@ -import { Divider } from '@/components/ui/divider'; -import { DropDown } from '@/components/ui/dropdown'; -import { DropdownPlacement } from '@/enums'; -import { ELEMENT_DISTANCE_FROM_NAVBAR } from '@/config'; -import { Link } from '@/components/ui/link'; -import { navLinks } from '@/constants/general'; -import { NavLogo } from '@/components/layout'; -import { useNavigate } from 'react-router-dom'; +import { Divider } from "@/components/ui/divider"; +import { DropDown } from "@/components/ui/dropdown"; +import { DropdownPlacement } from "@/enums"; +import { ELEMENT_DISTANCE_FROM_NAVBAR } from "@/config"; +import { Link } from "@/components/ui/link"; +import { navLinks } from "@/constants/general"; +import { NavLogo } from "@/components/layout"; +import { useNavigate } from "react-router-dom"; type BrandLogoWithDropDownProps = { isOpened: boolean; diff --git a/frontend/src/features/start-mapping/components/map/legend-control.tsx b/frontend/src/features/start-mapping/components/map/legend-control.tsx index 341a2286..fdf1fd2e 100644 --- a/frontend/src/features/start-mapping/components/map/legend-control.tsx +++ b/frontend/src/features/start-mapping/components/map/legend-control.tsx @@ -1,15 +1,9 @@ -import useScreenSize from '@/hooks/use-screen-size'; -import { LegendBookIcon } from '@/components/ui/icons'; -import { Map } from 'maplibre-gl'; -import { START_MAPPING_PAGE_CONTENT } from '@/constants'; -import { useCallback, useState } from 'react'; -import { - - LEGEND_NAME_MAPPING, - MAP_STYLES_PREFIX, -} from "@/config"; - - +import useScreenSize from "@/hooks/use-screen-size"; +import { LegendBookIcon } from "@/components/ui/icons"; +import { Map } from "maplibre-gl"; +import { START_MAPPING_PAGE_CONTENT } from "@/constants"; +import { useCallback, useState } from "react"; +import { LEGEND_NAME_MAPPING, MAP_STYLES_PREFIX } from "@/config"; const FillLegendStyle = ({ fillColor, diff --git a/frontend/src/features/start-mapping/components/map/map.tsx b/frontend/src/features/start-mapping/components/map/map.tsx index 2402d4c1..48c004af 100644 --- a/frontend/src/features/start-mapping/components/map/map.tsx +++ b/frontend/src/features/start-mapping/components/map/map.tsx @@ -1,13 +1,13 @@ -import PredictedFeatureActionPopup from '@/features/start-mapping/components/feature-popup'; -import useScreenSize from '@/hooks/use-screen-size'; -import { ControlsPosition } from '@/enums'; -import { extractTileJSONURL, showErrorToast } from '@/utils'; -import { GeoJSONSource, LngLatBoundsLike, Map } from 'maplibre-gl'; -import { Legend } from '@/features/start-mapping/components'; -import { MapComponent, MapCursorToolTip } from '@/components/map'; -import { TOAST_NOTIFICATIONS } from '@/constants'; -import { useMapLayers } from '@/hooks/use-map-layer'; -import { useToolTipVisibility } from '@/hooks/use-tooltip-visibility'; +import PredictedFeatureActionPopup from "@/features/start-mapping/components/feature-popup"; +import useScreenSize from "@/hooks/use-screen-size"; +import { ControlsPosition } from "@/enums"; +import { extractTileJSONURL, showErrorToast } from "@/utils"; +import { GeoJSONSource, LngLatBoundsLike, Map } from "maplibre-gl"; +import { Legend } from "@/features/start-mapping/components"; +import { MapComponent, MapCursorToolTip } from "@/components/map"; +import { TOAST_NOTIFICATIONS } from "@/constants"; +import { useMapLayers } from "@/hooks/use-map-layer"; +import { useToolTipVisibility } from "@/hooks/use-tooltip-visibility"; import { Dispatch, RefObject, @@ -25,7 +25,6 @@ import { TTrainingDataset, } from "@/types"; import { - ACCEPTED_MODEL_PREDICTIONS_FILL_LAYER_ID, ACCEPTED_MODEL_PREDICTIONS_OUTLINE_LAYER_ID, ACCEPTED_MODEL_PREDICTIONS_SOURCE_ID, @@ -39,7 +38,6 @@ import { REJECTED_MODEL_PREDICTIONS_SOURCE_ID, } from "@/config"; - export const StartMappingMapComponent = ({ trainingDataset, modelPredictions, @@ -53,9 +51,9 @@ export const StartMappingMapComponent = ({ currentZoom, layers, tmsBounds, - trainingId + trainingId, }: { - trainingId: number + trainingId: number; trainingDataset?: TTrainingDataset; modelPredictions: TModelPredictions; setModelPredictions: Dispatch< diff --git a/frontend/src/features/start-mapping/components/mobile-drawer.tsx b/frontend/src/features/start-mapping/components/mobile-drawer.tsx index a7491be4..556af391 100644 --- a/frontend/src/features/start-mapping/components/mobile-drawer.tsx +++ b/frontend/src/features/start-mapping/components/mobile-drawer.tsx @@ -1,18 +1,16 @@ -import ModelAction from '@/features/start-mapping/components/model-action'; -import { ChevronDownIcon, CloudDownloadIcon } from '@/components/ui/icons'; -import { Map } from 'maplibre-gl'; -import { MINIMUM_ZOOM_LEVEL_INSTRUCTION_FOR_PREDICTION } from '@/config'; -import { MobileDrawer } from '@/components/ui/drawer'; -import { ModelDetailsButton } from '@/features/start-mapping/components/model-details-button'; -import { ModelPredictionsTracker } from '@/features/start-mapping/components/model-predictions-tracker'; -import { ModelSettings } from '@/features/start-mapping/components/model-settings'; -import { TDownloadOptions, TQueryParams } from '@/app/routes/start-mapping'; -import { TModelPredictions, TModelPredictionsConfig } from '@/types'; -import { ToolTip } from '@/components/ui/tooltip'; -import { useState } from 'react'; -import { - START_MAPPING_PAGE_CONTENT, -} from "@/constants"; +import ModelAction from "@/features/start-mapping/components/model-action"; +import { ChevronDownIcon, CloudDownloadIcon } from "@/components/ui/icons"; +import { Map } from "maplibre-gl"; +import { MINIMUM_ZOOM_LEVEL_INSTRUCTION_FOR_PREDICTION } from "@/config"; +import { MobileDrawer } from "@/components/ui/drawer"; +import { ModelDetailsButton } from "@/features/start-mapping/components/model-details-button"; +import { ModelPredictionsTracker } from "@/features/start-mapping/components/model-predictions-tracker"; +import { ModelSettings } from "@/features/start-mapping/components/model-settings"; +import { TDownloadOptions, TQueryParams } from "@/app/routes/start-mapping"; +import { TModelPredictions, TModelPredictionsConfig } from "@/types"; +import { ToolTip } from "@/components/ui/tooltip"; +import { useState } from "react"; +import { START_MAPPING_PAGE_CONTENT } from "@/constants"; export const StartMappingMobileDrawer = ({ isOpen, @@ -87,8 +85,8 @@ export const StartMappingMobileDrawer = ({ content={ disablePrediction ? START_MAPPING_PAGE_CONTENT.actions.disabledModeTooltip( - "see download options", - ) + "see download options", + ) : null } > diff --git a/frontend/src/features/start-mapping/components/model-details-popup.tsx b/frontend/src/features/start-mapping/components/model-details-popup.tsx index 24b1b972..0066f472 100644 --- a/frontend/src/features/start-mapping/components/model-details-popup.tsx +++ b/frontend/src/features/start-mapping/components/model-details-popup.tsx @@ -1,15 +1,13 @@ -import useScreenSize from '@/hooks/use-screen-size'; -import { ELEMENT_DISTANCE_FROM_NAVBAR } from '@/config'; -import { extractDatePart, roundNumber, truncateString } from '@/utils'; -import { MobileDrawer } from '@/components/ui/drawer'; -import { Popup } from '@/components/ui/popup'; -import { SkeletonWrapper } from '@/components/ui/skeleton'; -import { TModelDetails, TTrainingDataset } from '@/types'; -import { useModelDetails } from '@/features/models/hooks/use-models'; -import { useTrainingDetails } from '@/features/models/hooks/use-training'; -import { - START_MAPPING_PAGE_CONTENT, -} from "@/constants"; +import useScreenSize from "@/hooks/use-screen-size"; +import { ELEMENT_DISTANCE_FROM_NAVBAR } from "@/config"; +import { extractDatePart, roundNumber, truncateString } from "@/utils"; +import { MobileDrawer } from "@/components/ui/drawer"; +import { Popup } from "@/components/ui/popup"; +import { SkeletonWrapper } from "@/components/ui/skeleton"; +import { TModelDetails, TTrainingDataset } from "@/types"; +import { useModelDetails } from "@/features/models/hooks/use-models"; +import { useTrainingDetails } from "@/features/models/hooks/use-training"; +import { START_MAPPING_PAGE_CONTENT } from "@/constants"; const ModelDetailsPopUp = ({ showPopup, diff --git a/frontend/src/features/start-mapping/components/model-settings.tsx b/frontend/src/features/start-mapping/components/model-settings.tsx index 42cba2ad..b2a0702c 100644 --- a/frontend/src/features/start-mapping/components/model-settings.tsx +++ b/frontend/src/features/start-mapping/components/model-settings.tsx @@ -1,19 +1,11 @@ -import { DropDown } from '@/components/ui/dropdown'; -import { ELEMENT_DISTANCE_FROM_NAVBAR } from '@/config'; -import { - FormLabel, - Input, - Select, - Switch - } from '@/components/ui/form'; -import { SEARCH_PARAMS, TQueryParams } from '@/app/routes/start-mapping'; -import { SettingsIcon } from '@/components/ui/icons'; -import { ToolTip } from '@/components/ui/tooltip'; -import { useDropdownMenu } from '@/hooks/use-dropdown-menu'; -import { - - START_MAPPING_PAGE_CONTENT, -} from "@/constants"; +import { DropDown } from "@/components/ui/dropdown"; +import { ELEMENT_DISTANCE_FROM_NAVBAR } from "@/config"; +import { FormLabel, Input, Select, Switch } from "@/components/ui/form"; +import { SEARCH_PARAMS, TQueryParams } from "@/app/routes/start-mapping"; +import { SettingsIcon } from "@/components/ui/icons"; +import { ToolTip } from "@/components/ui/tooltip"; +import { useDropdownMenu } from "@/hooks/use-dropdown-menu"; +import { START_MAPPING_PAGE_CONTENT } from "@/constants"; import { DropdownPlacement, @@ -96,7 +88,6 @@ export const ModelSettings = ({ options={confidenceLevels} defaultValue={query[SEARCH_PARAMS.confidenceLevel] as number} handleChange={(value) => { - handleQueryUpdate(SEARCH_PARAMS.confidenceLevel, Number(value)); }} /> diff --git a/frontend/src/features/start-mapping/hooks/use-feedbacks.ts b/frontend/src/features/start-mapping/hooks/use-feedbacks.ts index cb156a57..8a72e8f9 100644 --- a/frontend/src/features/start-mapping/hooks/use-feedbacks.ts +++ b/frontend/src/features/start-mapping/hooks/use-feedbacks.ts @@ -1,5 +1,5 @@ -import { MutationConfig } from '@/services'; -import { useMutation } from '@tanstack/react-query'; +import { MutationConfig } from "@/services"; +import { useMutation } from "@tanstack/react-query"; import { createApprovedPrediction, createFeedback, diff --git a/frontend/src/hooks/__tests__/use-login.test.ts b/frontend/src/hooks/__tests__/use-login.test.ts new file mode 100644 index 00000000..e78f40ec --- /dev/null +++ b/frontend/src/hooks/__tests__/use-login.test.ts @@ -0,0 +1,82 @@ +import { act, renderHook } from "@testing-library/react-hooks"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { authService } from "@/services"; +import { showErrorToast } from "@/utils"; +import { useLocation } from "react-router-dom"; +import { useLogin } from "../use-login"; +import { useSessionStorage } from "../use-storage"; +import { TOAST_NOTIFICATIONS } from "@/constants"; +import { HOT_FAIR_SESSION_REDIRECT_KEY } from "@/config"; + +vi.mock("react-router-dom", () => ({ + useLocation: vi.fn(), +})); + +vi.mock("../use-storage", () => ({ + useSessionStorage: vi.fn(), +})); + +vi.mock("@/services", () => ({ + authService: { + initializeOAuthFlow: vi.fn(), + }, +})); + +vi.mock("@/utils", () => ({ + showErrorToast: vi.fn(), +})); + +describe("useLogin", () => { + const setValueMock = vi.fn(); + const pathname = "/test-path"; + + beforeEach(() => { + (useLocation as vi.Mock).mockReturnValue({ pathname }); + + (useSessionStorage as vi.Mock).mockReturnValue({ + setSessionValue: setValueMock, + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it("should initialize with loading as false", () => { + const { result } = renderHook(() => useLogin()); + expect(result.current.loading).toBe(false); + }); + + it("should set loading to true and call authService.initializeOAuthFlow on handleLogin", async () => { + const { result } = renderHook(() => useLogin()); + + await act(async () => { + await result.current.handleLogin(); + }); + + expect(result.current.loading).toBe(false); + expect(setValueMock).toHaveBeenCalledWith( + HOT_FAIR_SESSION_REDIRECT_KEY, + pathname, + ); + expect(authService.initializeOAuthFlow).toHaveBeenCalled(); + }); + + it("should show error toast if authService.initializeOAuthFlow throws an error", async () => { + (authService.initializeOAuthFlow as vi.Mock).mockRejectedValue( + new Error("OAuth error"), + ); + + const { result } = renderHook(() => useLogin()); + + await act(async () => { + await result.current.handleLogin(); + }); + + expect(result.current.loading).toBe(false); + expect(showErrorToast).toHaveBeenCalledWith( + undefined, + TOAST_NOTIFICATIONS.authenticationFailed, + ); + }); +}); diff --git a/frontend/src/hooks/use-layer-order.ts b/frontend/src/hooks/use-layer-order.ts index 141131e0..6eadf993 100644 --- a/frontend/src/hooks/use-layer-order.ts +++ b/frontend/src/hooks/use-layer-order.ts @@ -1,6 +1,6 @@ -import { Map } from 'maplibre-gl'; -import { TILE_BOUNDARY_LAYER_ID, TMS_LAYER_ID } from '@/config'; -import { useEffect, useRef } from 'react'; +import { Map } from "maplibre-gl"; +import { TILE_BOUNDARY_LAYER_ID, TMS_LAYER_ID } from "@/config"; +import { useEffect, useRef } from "react"; type UseLayerReorderProps = { /** IDs of all feature layers. (We want them above TMS.) */ diff --git a/frontend/src/hooks/use-login.ts b/frontend/src/hooks/use-login.ts index 1bd495f3..af70a09f 100644 --- a/frontend/src/hooks/use-login.ts +++ b/frontend/src/hooks/use-login.ts @@ -1,12 +1,10 @@ -import { authService } from '@/services'; -import { HOT_FAIR_SESSION_REDIRECT_KEY } from '@/config'; -import { showErrorToast } from '@/utils'; -import { useLocation } from 'react-router-dom'; -import { useSessionStorage } from '@/hooks/use-storage'; -import { useState } from 'react'; -import { - TOAST_NOTIFICATIONS, -} from "@/constants"; +import { authService } from "@/services"; +import { HOT_FAIR_SESSION_REDIRECT_KEY } from "@/config"; +import { showErrorToast } from "@/utils"; +import { useLocation } from "react-router-dom"; +import { useSessionStorage } from "@/hooks/use-storage"; +import { useState } from "react"; +import { TOAST_NOTIFICATIONS } from "@/constants"; /** * Custom hook to handle the login button click event. diff --git a/frontend/src/hooks/use-screen-size.ts b/frontend/src/hooks/use-screen-size.ts index 147c4920..4357871e 100644 --- a/frontend/src/hooks/use-screen-size.ts +++ b/frontend/src/hooks/use-screen-size.ts @@ -14,10 +14,12 @@ const useScreenSize = () => { isMobile: boolean; isTablet: boolean; isLaptop: boolean; + isLargeScreen: boolean; }>({ isMobile: false, isTablet: false, isLaptop: false, + isLargeScreen: false, }); const handleResize = () => { @@ -25,6 +27,7 @@ const useScreenSize = () => { isMobile: window.innerWidth < 640, isTablet: window.innerWidth > 640 && window.innerWidth < 768, isLaptop: window.innerWidth > 768 && window.innerWidth < 1024, + isLargeScreen: window.innerWidth > 1300, }); }; diff --git a/frontend/src/hooks/use-storage.ts b/frontend/src/hooks/use-storage.ts index 96638948..b48f3a84 100644 --- a/frontend/src/hooks/use-storage.ts +++ b/frontend/src/hooks/use-storage.ts @@ -1,4 +1,4 @@ -import { showErrorToast } from '@/utils'; +import { showErrorToast } from "@/utils"; /** * Custom hook to interact with the browser's localStorage. @@ -77,5 +77,4 @@ export const useSessionStorage = () => { } }; return { getSessionValue, setSessionValue, removeSessionValue }; - }; diff --git a/frontend/src/layouts/model-forms-layout.tsx b/frontend/src/layouts/model-forms-layout.tsx index 7a7b742b..365cd8f3 100644 --- a/frontend/src/layouts/model-forms-layout.tsx +++ b/frontend/src/layouts/model-forms-layout.tsx @@ -1,8 +1,8 @@ -import { BackButton } from '@/components/ui/button'; -import { Head } from '@/components/seo'; -import { MODELS_CONTENT, MODELS_ROUTES } from '@/constants'; -import { Outlet, useLocation, useNavigate } from 'react-router-dom'; -import { useEffect, useState } from 'react'; +import { BackButton } from "@/components/ui/button"; +import { Head } from "@/components/seo"; +import { MODELS_CONTENT, MODELS_ROUTES } from "@/constants"; +import { Outlet, useLocation, useNavigate } from "react-router-dom"; +import { useEffect, useState } from "react"; import { ProgressBar, ProgressButtons, @@ -26,43 +26,43 @@ const pages: { icon: React.ElementType; path: string; }[] = [ - { - id: 1, - title: MODELS_CONTENT.modelCreation.progressStepper.modelDetails, - icon: TagsIcon, - path: MODELS_ROUTES.DETAILS, - }, - { - id: 2, - title: MODELS_CONTENT.modelCreation.progressStepper.trainingDataset, - icon: DatabaseIcon, - path: MODELS_ROUTES.TRAINING_DATASET, - }, - { - id: 3, - title: MODELS_CONTENT.modelCreation.progressStepper.trainingArea, - icon: SquareShadowIcon, - path: MODELS_ROUTES.TRAINING_AREA, - }, - { - id: 4, - title: MODELS_CONTENT.modelCreation.progressStepper.trainingSettings, - icon: SettingsIcon, - path: MODELS_ROUTES.TRAINING_SETTINGS, - }, - { - id: 5, - title: MODELS_CONTENT.modelCreation.progressStepper.submitModel, - icon: CloudIcon, - path: MODELS_ROUTES.MODEL_SUMMARY, - }, - { - id: 6, - title: MODELS_CONTENT.modelCreation.progressStepper.confirmation, - icon: StarIcon, - path: MODELS_ROUTES.CONFIRMATION, - }, - ]; + { + id: 1, + title: MODELS_CONTENT.modelCreation.progressStepper.modelDetails, + icon: TagsIcon, + path: MODELS_ROUTES.DETAILS, + }, + { + id: 2, + title: MODELS_CONTENT.modelCreation.progressStepper.trainingDataset, + icon: DatabaseIcon, + path: MODELS_ROUTES.TRAINING_DATASET, + }, + { + id: 3, + title: MODELS_CONTENT.modelCreation.progressStepper.trainingArea, + icon: SquareShadowIcon, + path: MODELS_ROUTES.TRAINING_AREA, + }, + { + id: 4, + title: MODELS_CONTENT.modelCreation.progressStepper.trainingSettings, + icon: SettingsIcon, + path: MODELS_ROUTES.TRAINING_SETTINGS, + }, + { + id: 5, + title: MODELS_CONTENT.modelCreation.progressStepper.submitModel, + icon: CloudIcon, + path: MODELS_ROUTES.MODEL_SUMMARY, + }, + { + id: 6, + title: MODELS_CONTENT.modelCreation.progressStepper.confirmation, + icon: StarIcon, + path: MODELS_ROUTES.CONFIRMATION, + }, +]; export const ModelFormsLayout = () => { const { pathname } = useLocation(); diff --git a/frontend/src/layouts/root-layout.tsx b/frontend/src/layouts/root-layout.tsx index 538d1eae..accaa32c 100644 --- a/frontend/src/layouts/root-layout.tsx +++ b/frontend/src/layouts/root-layout.tsx @@ -6,7 +6,8 @@ import { NavBar } from "@/components/layout"; import { Outlet, useLocation } from "react-router-dom"; import { useEffect } from "react"; import { useScrollToTop } from "@/hooks/use-scroll-to-element"; -import { OpenStreetMapAuthModal } from "@/components/auth"; +import { useAuth } from "@/app/providers/auth-provider"; +import { AuthenticationModal } from "@/components/auth"; export const RootLayout = () => { const { pathname, state } = useLocation(); @@ -15,26 +16,21 @@ export const RootLayout = () => { useEffect(() => { scrollToTop(); }, [pathname]); - + const { isAuthenticated } = useAuth(); const pagesWithoutNavbarAndFooter = [ - APPLICATION_ROUTES.OSM_LOGIN, - APPLICATION_ROUTES.START_MAPPING_BASE + APPLICATION_ROUTES.AUTH_CALLBACK, + APPLICATION_ROUTES.START_MAPPING_BASE, ]; - const pagesWithoutPadding = [ - APPLICATION_ROUTES.HOMEPAGE - ] + const pagesWithoutPadding = [APPLICATION_ROUTES.HOMEPAGE]; return ( <> - { - state?.backgroundLocation && - } + {state?.backgroundLocation && !isAuthenticated && }
- - {!pagesWithoutNavbarAndFooter.includes(pathname) && ( - - )} + {!pagesWithoutNavbarAndFooter.includes(pathname) && } + + {!pagesWithoutNavbarAndFooter.includes(pathname) && }
{ >
- {!pagesWithoutNavbarAndFooter.includes(pathname) && ( -
- )} + {!pagesWithoutNavbarAndFooter.includes(pathname) &&
}
); diff --git a/frontend/src/services/__tests__/auth.test.ts b/frontend/src/services/__tests__/auth.test.ts new file mode 100644 index 00000000..4768cb13 --- /dev/null +++ b/frontend/src/services/__tests__/auth.test.ts @@ -0,0 +1,119 @@ +import { describe, it, expect, vi, afterEach } from "vitest"; +import { authService } from "../auth"; +import { apiClient } from "@/services/api-client"; +import { showErrorToast } from "@/utils"; +import { API_ENDPOINTS } from "@/services/api-routes"; + +vi.mock("@/services/api-client"); +vi.mock("@/utils"); + +describe("AuthService", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + describe("getOAuthURL", () => { + it("should return the OAuth URL", async () => { + const mockResponse = { data: { login_url: "http://example.com" } }; + apiClient.get.mockResolvedValue(mockResponse); + + const result = await authService.getOAuthURL(); + + expect(result).toEqual(mockResponse.data); + expect(apiClient.get).toHaveBeenCalledWith(API_ENDPOINTS.LOGIN); + }); + + it("should show error toast and throw error on failure", async () => { + apiClient.get.mockRejectedValue(new Error("Network Error")); + + await expect(authService.getOAuthURL()).rejects.toThrow( + "Unable to retrieve login URL.", + ); + expect(showErrorToast).toHaveBeenCalledWith( + undefined, + "Failed to get OAuth URL", + ); + }); + }); + + describe("initializeOAuthFlow", () => { + it("should open a popup with the OAuth URL", async () => { + const mockOAuthURL = { login_url: "http://example.com" }; + authService.getOAuthURL = vi.fn().mockResolvedValue(mockOAuthURL); + window.open = vi.fn().mockReturnValue({}); + + await authService.initializeOAuthFlow(); + + expect(authService.getOAuthURL).toHaveBeenCalled(); + expect(window.open).toHaveBeenCalledWith( + mockOAuthURL.login_url, + "_parent", + expect.any(String), + ); + }); + + it("should show error toast and throw error if popup is blocked", async () => { + const mockOAuthURL = { login_url: "http://example.com" }; + authService.getOAuthURL = vi.fn().mockResolvedValue(mockOAuthURL); + window.open = vi.fn().mockReturnValue(null); + + await expect(authService.initializeOAuthFlow()).rejects.toThrow( + "Popup blocked or not created.", + ); + expect(showErrorToast).toHaveBeenCalledWith( + undefined, + "OAuth flow initialization failed", + ); + }); + }); + + describe("getUser", () => { + it("should return the user data", async () => { + const mockResponse = { data: { id: 1, name: "John Doe" } }; + apiClient.get.mockResolvedValue(mockResponse); + + const result = await authService.getUser(); + + expect(result).toEqual(mockResponse.data); + expect(apiClient.get).toHaveBeenCalledWith(API_ENDPOINTS.USER); + }); + + it("should show error toast and throw error on failure", async () => { + apiClient.get.mockRejectedValue(new Error("Network Error")); + + await expect(authService.getUser()).rejects.toThrow( + "Unable to retrieve user data.", + ); + expect(showErrorToast).toHaveBeenCalledWith( + undefined, + "Failed to fetch user data", + ); + }); + }); + + describe("authenticate", () => { + it("should return the authentication data", async () => { + const mockResponse = { data: { access_token: "token" } }; + apiClient.get.mockResolvedValue(mockResponse); + + const result = await authService.authenticate("state", "code"); + + expect(result).toEqual(mockResponse.data); + expect(apiClient.get).toHaveBeenCalledWith( + `${API_ENDPOINTS.AUTH_CALLBACK}?code=code&state=state`, + ); + }); + + it("should show error toast and throw error on failure", async () => { + apiClient.get.mockRejectedValue(new Error("Network Error")); + + await expect(authService.authenticate("state", "code")).rejects.toThrow( + "Failed to authenticate user.", + ); + expect(showErrorToast).toHaveBeenCalledWith( + undefined, + "Authentication failed", + ); + }); + }); +}); diff --git a/frontend/src/services/api-client.ts b/frontend/src/services/api-client.ts index 4eaf32c3..53a8676b 100644 --- a/frontend/src/services/api-client.ts +++ b/frontend/src/services/api-client.ts @@ -1,6 +1,9 @@ -import Axios, { InternalAxiosRequestConfig } from 'axios'; -import { BASE_API_URL, HOT_FAIR_LOCAL_STORAGE_ACCESS_TOKEN_KEY } from '@/config'; -import { showErrorToast } from '@/utils'; +import Axios, { InternalAxiosRequestConfig } from "axios"; +import { + BASE_API_URL, + HOT_FAIR_LOCAL_STORAGE_ACCESS_TOKEN_KEY, +} from "@/config"; +import { showErrorToast } from "@/utils"; /** * The global axios API client. diff --git a/frontend/src/services/api-routes.ts b/frontend/src/services/api-routes.ts index 0630952f..3000eab3 100644 --- a/frontend/src/services/api-routes.ts +++ b/frontend/src/services/api-routes.ts @@ -1,4 +1,7 @@ -import { FAIR_PREDICTOR_API_ENDPOINT, OSM_DATABASE_STATUS_API_ENDPOINT } from '@/config'; +import { + FAIR_PREDICTOR_API_ENDPOINT, + OSM_DATABASE_STATUS_API_ENDPOINT, +} from "@/config"; /** * The backend API endpoints. diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts index f824f4ec..46dd7680 100644 --- a/frontend/src/types/api.ts +++ b/frontend/src/types/api.ts @@ -1,5 +1,5 @@ -import { BBOX } from './common'; -import { GeoJsonProperties, Geometry } from 'geojson'; +import { BBOX } from "./common"; +import { GeoJsonProperties, Geometry } from "geojson"; /** * This file contains the different types/schema for the API responses from the backend. @@ -159,10 +159,10 @@ export type Feature = { type: "Feature"; geometry: Geometry; properties: - | { - mid: string; - } - | GeoJsonProperties; + | { + mid: string; + } + | GeoJsonProperties; }; export type FeatureCollection = { @@ -184,8 +184,6 @@ export type TModelPredictionsConfig = { zoom_level: number; }; - - export type TModelPredictionFeature = { type: "Feature"; geometry: Geometry; diff --git a/frontend/src/types/ui-contents.ts b/frontend/src/types/ui-contents.ts index 6c72af10..5f77c180 100644 --- a/frontend/src/types/ui-contents.ts +++ b/frontend/src/types/ui-contents.ts @@ -1,4 +1,4 @@ -import { IconProps } from './common'; +import { IconProps } from "./common"; // Models related pages content types starts. export type TModelsContent = { @@ -102,7 +102,6 @@ export type TModelsContent = { title: string; mainInstruction: string; fleSizeInstruction: string; - aoiAreaInstruction: string; }; pageDescription: string; }; @@ -591,6 +590,16 @@ export type TStartMappingPageContent = { // Start mapping page content types ends. +// Auth Page/Modal content starts. +export type TAuthPageAndModalContent = { + pageTitle: string; + title: string; + instruction: string; + buttonText: string; + authInProgressText: string; +}; +// Auth Page/Modal content ends. + // About page content types starts. export type TAboutPageContent = { diff --git a/frontend/src/utils/geo/geo-utils.ts b/frontend/src/utils/geo/geo-utils.ts index 0e9bcc49..13e62653 100644 --- a/frontend/src/utils/geo/geo-utils.ts +++ b/frontend/src/utils/geo/geo-utils.ts @@ -1,18 +1,17 @@ -import bbox from '@turf/bbox'; +import bbox from "@turf/bbox"; import { BASE_API_URL, JOSM_REMOTE_URL, MAX_TRAINING_AREA_SIZE, MIN_TRAINING_AREA_SIZE, - OSM_HASHTAGS - } from '@/config'; -import { calculateGeoJSONArea } from './geometry-utils'; -import { Feature, FeatureCollection } from 'geojson'; -import { geojsonToOsmPolygons } from './geojson-to-osm'; -import { showErrorToast, showSuccessToast } from '../general-utils'; -import { TOAST_NOTIFICATIONS } from '@/constants'; -import { API_ENDPOINTS, } from '@/services'; - + OSM_HASHTAGS, +} from "@/config"; +import { calculateGeoJSONArea } from "./geometry-utils"; +import { Feature, FeatureCollection } from "geojson"; +import { geojsonToOsmPolygons } from "./geojson-to-osm"; +import { showErrorToast, showSuccessToast } from "../general-utils"; +import { TOAST_NOTIFICATIONS } from "@/constants"; +import { API_ENDPOINTS } from "@/services"; /** * Creates a GeoJSON FeatureCollection @@ -127,7 +126,10 @@ export const openInJOSM = async ( loadurl.searchParams.set("top", String(bounds[3])); loadurl.searchParams.set("left", String(bounds[0])); loadurl.searchParams.set("right", String(bounds[2])); - loadurl.searchParams.set("changeset_tags", `comment=${OSM_HASHTAGS}|source=${oamTileName}`); + loadurl.searchParams.set( + "changeset_tags", + `comment=${OSM_HASHTAGS}|source=${oamTileName}`, + ); await fetch(loadurl); showSuccessToast(TOAST_NOTIFICATIONS.josmOpenSuccess); } catch (error) { diff --git a/frontend/src/utils/geo/geojson-to-osm.ts b/frontend/src/utils/geo/geojson-to-osm.ts index acaae49f..d34d7937 100644 --- a/frontend/src/utils/geo/geojson-to-osm.ts +++ b/frontend/src/utils/geo/geojson-to-osm.ts @@ -1,5 +1,5 @@ -import { create } from 'xmlbuilder2'; -import { FeatureCollection, Position } from 'geojson'; +import { create } from "xmlbuilder2"; +import { FeatureCollection, Position } from "geojson"; class Node { lat: number; @@ -27,7 +27,6 @@ class Way { } } - export const geojsonToOsmPolygons = (geojson: FeatureCollection): string => { if (!geojson || geojson.type !== "FeatureCollection") { throw new Error("Invalid GeoJSON FeatureCollection"); @@ -37,7 +36,6 @@ export const geojsonToOsmPolygons = (geojson: FeatureCollection): string => { const nodesIndex: Record = {}; const ways: Way[] = []; - geojson.features.forEach((feature) => { const { geometry, properties } = feature; @@ -61,7 +59,6 @@ export const geojsonToOsmPolygons = (geojson: FeatureCollection): string => { generator: "HOT-fAIr", }); - let lastNodeId = -1; nodes.forEach((node) => { node.id = lastNodeId--; @@ -128,9 +125,8 @@ const processPolygon = ( }); }; - const mapPropertiesToTags = ( - properties: Record + properties: Record, ): Record => { const tags: Record = {}; diff --git a/frontend/src/utils/geo/geometry-utils.ts b/frontend/src/utils/geo/geometry-utils.ts index fa355fd8..03134cd4 100644 --- a/frontend/src/utils/geo/geometry-utils.ts +++ b/frontend/src/utils/geo/geometry-utils.ts @@ -1,17 +1,12 @@ -import area from '@turf/area'; -import bboxPolygon from '@turf/bbox'; -import { booleanIntersects } from '@turf/boolean-intersects'; -import { createFeatureCollection } from './geo-utils'; -import { - Feature, - FeatureCollection, - Polygon, - Position - } from 'geojson'; -import { LngLatBoundsLike, Map } from 'maplibre-gl'; -import { roundNumber } from '../number-utils'; -import { TModelPredictions, TModelPredictionsConfig } from '@/types'; -import { uuid4 } from '../general-utils'; +import area from "@turf/area"; +import bboxPolygon from "@turf/bbox"; +import { booleanIntersects } from "@turf/boolean-intersects"; +import { createFeatureCollection } from "./geo-utils"; +import { Feature, FeatureCollection, Polygon, Position } from "geojson"; +import { LngLatBoundsLike, Map } from "maplibre-gl"; +import { roundNumber } from "../number-utils"; +import { TModelPredictions, TModelPredictionsConfig } from "@/types"; +import { uuid4 } from "../general-utils"; /** * Calculates the area of a GeoJSON Feature or FeatureCollection. @@ -44,10 +39,15 @@ export const calculateGeoJSONArea = ( export function formatAreaInAppropriateUnit(area: number) { const SQUARE_METERS_IN_SQUARE_KILOMETER = 1000000; if (area > SQUARE_METERS_IN_SQUARE_KILOMETER) { - return roundNumber(area / SQUARE_METERS_IN_SQUARE_KILOMETER, 1).toLocaleString() + "km²"; + return ( + roundNumber( + area / SQUARE_METERS_IN_SQUARE_KILOMETER, + 1, + ).toLocaleString() + "km²" + ); } return roundNumber(area, 1).toLocaleString() + "m²"; -}; +} /** * Computes the bounding box of a GeoJSON Feature. diff --git a/frontend/src/utils/regex-utils.ts b/frontend/src/utils/regex-utils.ts index 0bc34a6d..7e860b0b 100644 --- a/frontend/src/utils/regex-utils.ts +++ b/frontend/src/utils/regex-utils.ts @@ -1,5 +1,3 @@ export const TMS_URL_REGEX_PATTERN = /^https:\/\/.*\/\{z\}\/\{x\}\/\{y\}.*$/; - export const VALID_CHARACTER_PATTERN = /^[a-zA-Z0-9\s]*$/; // Allows letters, numbers, and spaces - diff --git a/frontend/src/utils/string-utils.ts b/frontend/src/utils/string-utils.ts index 18bcdf8c..8599d6ef 100644 --- a/frontend/src/utils/string-utils.ts +++ b/frontend/src/utils/string-utils.ts @@ -1,4 +1,4 @@ -import { OAM_S3_BUCKET_URL, OAM_TITILER_ENDPOINT } from '@/config'; +import { OAM_S3_BUCKET_URL, OAM_TITILER_ENDPOINT } from "@/config"; /** * Truncates a string to a specified maximum length, appending ellipsis if truncated. @@ -18,15 +18,14 @@ export const truncateString = (string?: string, maxLength: number = 30) => { return string; }; - export const extractTileJSONURL = (OAMTMSURL: string) => { - - // Before, when we hit this url https://tiles.openaerialmap.org/63b457ba3fb8c100063c55f0/0/63b457ba3fb8c100063c55f1/{z}/{x}/{y} (without the /{z}/{x}/{y}), + // Before, when we hit this url https://tiles.openaerialmap.org/63b457ba3fb8c100063c55f0/0/63b457ba3fb8c100063c55f1/{z}/{x}/{y} (without the /{z}/{x}/{y}), // we get the TileJSON which is passed to Maplibre GL JS to render the aerial imagery, but with the recent OAM updates - // we have to grab the unique id of the aerial imagery, construct the new S3 bucket location and give it to titiler to get the new TileJSON. - const uniqueImageryId = OAMTMSURL - .replace('https://tiles.openaerialmap.org/', '') - .replace('/{z}/{x}/{y}', ''); + // we have to grab the unique id of the aerial imagery, construct the new S3 bucket location and give it to titiler to get the new TileJSON. + const uniqueImageryId = OAMTMSURL.replace( + "https://tiles.openaerialmap.org/", + "", + ).replace("/{z}/{x}/{y}", ""); // Construct the URL to fetch the TileJSON from titiler. return `${OAM_TITILER_ENDPOINT}cog/WebMercatorQuad/tilejson.json?url=${OAM_S3_BUCKET_URL}${uniqueImageryId}.tif`; diff --git a/frontend/test-setup.ts b/frontend/test-setup.ts new file mode 100644 index 00000000..eff3639c --- /dev/null +++ b/frontend/test-setup.ts @@ -0,0 +1,6 @@ +import { vi } from 'vitest'; + + +if (!globalThis.URL.createObjectURL) { + globalThis.URL.createObjectURL = vi.fn(() => 'blob:mock-blob'); +} \ No newline at end of file diff --git a/frontend/vite.config.mts b/frontend/vite.config.mts index abed9da3..8d1de1bc 100644 --- a/frontend/vite.config.mts +++ b/frontend/vite.config.mts @@ -2,12 +2,22 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import tsconfigPaths from "vite-tsconfig-paths"; + +/// + + // https://vitejs.dev/config/ export default defineConfig({ plugins: [react(), tsconfigPaths()], + // By default it was localhost:5173, but it was causing some issues with the OAUTH, so it was changed to this. server: { host: "127.0.0.1", port: 5173, }, + + test: { + environment: 'jsdom', + setupFiles: ['./test-setup.ts'], + } }); From d45db1681fc639e8b2c97affd9457efa25e87a27 Mon Sep 17 00:00:00 2001 From: jeafreezy Date: Mon, 24 Feb 2025 19:15:48 +0100 Subject: [PATCH 07/12] fix: fixed typescript bug --- frontend/src/components/auth/auth-modal.tsx | 2 +- frontend/src/constants/ui-contents/models-content.ts | 3 +-- frontend/src/hooks/__tests__/use-login.test.ts | 1 + frontend/src/services/__tests__/auth.test.ts | 3 +++ frontend/vite.config.mts | 2 +- 5 files changed, 7 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/auth/auth-modal.tsx b/frontend/src/components/auth/auth-modal.tsx index 03405dbe..376dc3c7 100644 --- a/frontend/src/components/auth/auth-modal.tsx +++ b/frontend/src/components/auth/auth-modal.tsx @@ -13,7 +13,7 @@ import { Spinner } from "@/components/ui/spinner"; export const AuthenticationModal = ({ callbackPage = false, }: { - callbackPage: boolean; + callbackPage?: boolean; }) => { const { closeDialog } = useDialog(); const navigate = useNavigate(); diff --git a/frontend/src/constants/ui-contents/models-content.ts b/frontend/src/constants/ui-contents/models-content.ts index 47ff69c1..1e852fe2 100644 --- a/frontend/src/constants/ui-contents/models-content.ts +++ b/frontend/src/constants/ui-contents/models-content.ts @@ -1,6 +1,5 @@ import { BASE_MODELS } from "@/enums"; -import { formatAreaInAppropriateUnit } from "@/utils"; -import { MAX_TRAINING_AREA_SIZE, MIN_TRAINING_AREA_SIZE } from "@/config"; + import { TModelsContent } from "@/types"; export const MODELS_CONTENT: TModelsContent = { diff --git a/frontend/src/hooks/__tests__/use-login.test.ts b/frontend/src/hooks/__tests__/use-login.test.ts index e78f40ec..7095b266 100644 --- a/frontend/src/hooks/__tests__/use-login.test.ts +++ b/frontend/src/hooks/__tests__/use-login.test.ts @@ -1,3 +1,4 @@ +// @ts-nocheck import { act, renderHook } from "@testing-library/react-hooks"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { authService } from "@/services"; diff --git a/frontend/src/services/__tests__/auth.test.ts b/frontend/src/services/__tests__/auth.test.ts index 4768cb13..475e7af9 100644 --- a/frontend/src/services/__tests__/auth.test.ts +++ b/frontend/src/services/__tests__/auth.test.ts @@ -1,3 +1,5 @@ +// @ts-nocheck + import { describe, it, expect, vi, afterEach } from "vitest"; import { authService } from "../auth"; import { apiClient } from "@/services/api-client"; @@ -15,6 +17,7 @@ describe("AuthService", () => { describe("getOAuthURL", () => { it("should return the OAuth URL", async () => { const mockResponse = { data: { login_url: "http://example.com" } }; + apiClient.get.mockResolvedValue(mockResponse); const result = await authService.getOAuthURL(); diff --git a/frontend/vite.config.mts b/frontend/vite.config.mts index 8d1de1bc..a2a54d09 100644 --- a/frontend/vite.config.mts +++ b/frontend/vite.config.mts @@ -1,4 +1,4 @@ -import { defineConfig } from "vite"; +import { defineConfig } from "vitest/config"; import react from "@vitejs/plugin-react"; import tsconfigPaths from "vite-tsconfig-paths"; From 4c293fde90b2bc003e5a2ec51bf2c2c1b8bc0126 Mon Sep 17 00:00:00 2001 From: jeafreezy Date: Mon, 24 Feb 2025 19:26:11 +0100 Subject: [PATCH 08/12] chore: fixed testing library version --- frontend/package.json | 2 +- frontend/pnpm-lock.yaml | 102 +++++++++++++----- .../src/hooks/__tests__/use-login.test.ts | 2 +- 3 files changed, 77 insertions(+), 29 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 6cebdb3d..9301a3d9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -47,7 +47,7 @@ "@eslint/js": "^9.9.0", "@tailwindcss/typography": "^0.5.15", "@tanstack/eslint-plugin-query": "^5.58.1", - "@testing-library/react-hooks": "^8.0.1", + "@testing-library/react": "^16.2.0", "@types/geojson": "^7946.0.14", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 2ec18ab0..0d508e83 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -106,9 +106,9 @@ importers: '@tanstack/eslint-plugin-query': specifier: ^5.58.1 version: 5.58.1(eslint@9.11.1(jiti@1.21.6))(typescript@5.6.2) - '@testing-library/react-hooks': - specifier: ^8.0.1 - version: 8.0.1(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@testing-library/react': + specifier: ^16.2.0 + version: 16.2.0(@testing-library/dom@10.4.0)(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/geojson': specifier: ^7946.0.14 version: 7946.0.14 @@ -913,20 +913,23 @@ packages: '@terraformer/wkt@2.2.1': resolution: {integrity: sha512-XDUsW/lvbMzFi7GIuRD9+UqR4QyP+5C+TugeJLMDczKIRbaHoE9J3N8zLSdyOGmnJL9B6xTS3YMMlBnMU0Ar5A==} - '@testing-library/react-hooks@8.0.1': - resolution: {integrity: sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g==} - engines: {node: '>=12'} + '@testing-library/dom@10.4.0': + resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} + engines: {node: '>=18'} + + '@testing-library/react@16.2.0': + resolution: {integrity: sha512-2cSskAvA1QNtKc8Y9VJQRv0tm3hLVgxRGDB+KYhIaPQJ1I+RHbhIXcM+zClKXzMes/wshsMVzf4B9vS4IZpqDQ==} + engines: {node: '>=18'} peerDependencies: - '@types/react': ^16.9.0 || ^17.0.0 - react: ^16.9.0 || ^17.0.0 - react-dom: ^16.9.0 || ^17.0.0 - react-test-renderer: ^16.9.0 || ^17.0.0 + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 peerDependenciesMeta: '@types/react': optional: true - react-dom: - optional: true - react-test-renderer: + '@types/react-dom': optional: true '@turf/area@7.1.0': @@ -959,6 +962,9 @@ packages: '@turf/polygon-to-line@7.1.0': resolution: {integrity: sha512-FBlfyBWNQZCTVGqlJH7LR2VXmvj8AydxrA8zegqek/5oPGtQDeUgIppKmvmuNClqbglhv59QtCUVaDK4bOuCTA==} + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -1206,6 +1212,10 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + ansi-styles@6.2.1: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} @@ -1230,6 +1240,9 @@ packages: resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==} engines: {node: '>=10'} + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + aria-query@5.3.2: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} @@ -1540,6 +1553,9 @@ packages: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -2339,6 +2355,10 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} @@ -2716,6 +2736,10 @@ packages: engines: {node: '>=14'} hasBin: true + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} @@ -2764,12 +2788,6 @@ packages: peerDependencies: react: '>= 16.8 || 18.0.0' - react-error-boundary@3.1.4: - resolution: {integrity: sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==} - engines: {node: '>=10', npm: '>=6'} - peerDependencies: - react: '>=16.13.1' - react-error-boundary@4.0.13: resolution: {integrity: sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ==} peerDependencies: @@ -2786,6 +2804,9 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-jss@10.10.0: resolution: {integrity: sha512-WLiq84UYWqNBF6579/uprcIUnM1TSywYq6AIjKTTTG5ziJl9Uy+pwuvpN3apuyVwflMbD60PraeTKT7uWH9XEQ==} peerDependencies: @@ -4142,14 +4163,26 @@ snapshots: '@terraformer/wkt@2.2.1': {} - '@testing-library/react-hooks@8.0.1(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@testing-library/dom@10.4.0': dependencies: + '@babel/code-frame': 7.24.7 '@babel/runtime': 7.25.6 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + chalk: 4.1.2 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + pretty-format: 27.5.1 + + '@testing-library/react@16.2.0(@testing-library/dom@10.4.0)(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.25.6 + '@testing-library/dom': 10.4.0 react: 18.3.1 - react-error-boundary: 3.1.4(react@18.3.1) + react-dom: 18.3.1(react@18.3.1) optionalDependencies: '@types/react': 18.3.10 - react-dom: 18.3.1(react@18.3.1) + '@types/react-dom': 18.3.0 '@turf/area@7.1.0': dependencies: @@ -4221,6 +4254,8 @@ snapshots: '@types/geojson': 7946.0.14 tslib: 2.7.0 + '@types/aria-query@5.0.4': {} + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.25.6 @@ -4543,6 +4578,8 @@ snapshots: dependencies: color-convert: 2.0.1 + ansi-styles@5.2.0: {} + ansi-styles@6.2.1: {} any-promise@1.3.0: {} @@ -4564,6 +4601,10 @@ snapshots: dependencies: tslib: 2.7.0 + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + aria-query@5.3.2: {} arr-union@3.1.0: {} @@ -4889,6 +4930,8 @@ snapshots: dependencies: esutils: 2.0.3 + dom-accessibility-api@0.5.16: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.1 @@ -5854,6 +5897,8 @@ snapshots: dependencies: yallist: 3.1.1 + lz-string@1.5.0: {} + magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -6451,6 +6496,12 @@ snapshots: prettier@3.3.3: {} + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 @@ -6495,11 +6546,6 @@ snapshots: prop-types: 15.8.1 react: 18.3.1 - react-error-boundary@3.1.4(react@18.3.1): - dependencies: - '@babel/runtime': 7.25.6 - react: 18.3.1 - react-error-boundary@4.0.13(react@18.3.1): dependencies: '@babel/runtime': 7.25.6 @@ -6516,6 +6562,8 @@ snapshots: react-is@16.13.1: {} + react-is@17.0.2: {} + react-jss@10.10.0(react@18.3.1): dependencies: '@babel/runtime': 7.25.6 diff --git a/frontend/src/hooks/__tests__/use-login.test.ts b/frontend/src/hooks/__tests__/use-login.test.ts index 7095b266..009c0366 100644 --- a/frontend/src/hooks/__tests__/use-login.test.ts +++ b/frontend/src/hooks/__tests__/use-login.test.ts @@ -1,5 +1,5 @@ // @ts-nocheck -import { act, renderHook } from "@testing-library/react-hooks"; +import { act, renderHook } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { authService } from "@/services"; import { showErrorToast } from "@/utils"; From faaba138ac211f4c52364d916c9765eff08ad2ec Mon Sep 17 00:00:00 2001 From: jeafreezy Date: Mon, 24 Feb 2025 23:20:31 +0100 Subject: [PATCH 09/12] chore: refactored modal to enable smooth open animation --- frontend/src/app/routes/authenticate.tsx | 2 +- frontend/src/components/auth/auth-modal.tsx | 4 +++- frontend/src/layouts/root-layout.tsx | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/routes/authenticate.tsx b/frontend/src/app/routes/authenticate.tsx index 4900c03c..fa95fff0 100644 --- a/frontend/src/app/routes/authenticate.tsx +++ b/frontend/src/app/routes/authenticate.tsx @@ -26,7 +26,7 @@ export const AuthenticationCallbackPage = () => { return ( <> - + ); }; diff --git a/frontend/src/components/auth/auth-modal.tsx b/frontend/src/components/auth/auth-modal.tsx index 376dc3c7..34291648 100644 --- a/frontend/src/components/auth/auth-modal.tsx +++ b/frontend/src/components/auth/auth-modal.tsx @@ -12,8 +12,10 @@ import { Spinner } from "@/components/ui/spinner"; export const AuthenticationModal = ({ callbackPage = false, + isOpen = false }: { callbackPage?: boolean; + isOpen?: boolean }) => { const { closeDialog } = useDialog(); const navigate = useNavigate(); @@ -25,7 +27,7 @@ export const AuthenticationModal = ({ }; return ( - +
diff --git a/frontend/src/layouts/root-layout.tsx b/frontend/src/layouts/root-layout.tsx index accaa32c..d63a69e0 100644 --- a/frontend/src/layouts/root-layout.tsx +++ b/frontend/src/layouts/root-layout.tsx @@ -26,7 +26,7 @@ export const RootLayout = () => { return ( <> - {state?.backgroundLocation && !isAuthenticated && } +
{!pagesWithoutNavbarAndFooter.includes(pathname) && } From 865590c57db23df8522cbc7a275f1e0cea5a856a Mon Sep 17 00:00:00 2001 From: jeafreezy Date: Mon, 24 Feb 2025 23:33:10 +0100 Subject: [PATCH 10/12] chore: added comments --- frontend/src/app/providers/auth-provider.tsx | 7 ++++++- frontend/src/app/routes/protected-route.tsx | 9 ++++++--- frontend/src/components/auth/auth-modal.tsx | 4 ++-- .../src/components/layout/navbar/navbar.tsx | 18 ++++++++++++------ frontend/src/hooks/__tests__/use-login.test.ts | 2 +- frontend/src/layouts/root-layout.tsx | 5 ++++- 6 files changed, 31 insertions(+), 14 deletions(-) diff --git a/frontend/src/app/providers/auth-provider.tsx b/frontend/src/app/providers/auth-provider.tsx index c2b1824a..c9fef710 100644 --- a/frontend/src/app/providers/auth-provider.tsx +++ b/frontend/src/app/providers/auth-provider.tsx @@ -9,7 +9,7 @@ import { import { showErrorToast, showSuccessToast } from "@/utils"; import { TUser } from "@/types/api"; import { useLocalStorage, useSessionStorage } from "@/hooks/use-storage"; -import { TOAST_NOTIFICATIONS } from "@/constants"; +import { APPLICATION_ROUTES, TOAST_NOTIFICATIONS } from "@/constants"; type TAuthContext = { token: string; @@ -126,6 +126,11 @@ export const AuthProvider: React.FC = ({ children }) => { setToken(data.access_token); } catch (error) { showErrorToast(error, TOAST_NOTIFICATIONS.authenticationFailed); + // Delay for 3 seconds, incase it's the network speed. + // Otherwise, redirect the user back to the home page. + setTimeout(() => { + window.location.href = APPLICATION_ROUTES.HOMEPAGE; + }, 3000); } }; diff --git a/frontend/src/app/routes/protected-route.tsx b/frontend/src/app/routes/protected-route.tsx index 9a4ccada..9ba4c63a 100644 --- a/frontend/src/app/routes/protected-route.tsx +++ b/frontend/src/app/routes/protected-route.tsx @@ -33,9 +33,12 @@ export const ProtectedRoute: React.FC = ({ children }) => {
@@ -68,11 +71,14 @@ export const NavBar = () => { diff --git a/frontend/src/hooks/__tests__/use-login.test.ts b/frontend/src/hooks/__tests__/use-login.test.ts index 009c0366..88a5aab4 100644 --- a/frontend/src/hooks/__tests__/use-login.test.ts +++ b/frontend/src/hooks/__tests__/use-login.test.ts @@ -1,4 +1,4 @@ -// @ts-nocheck +// @ts-nocheck import { act, renderHook } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { authService } from "@/services"; diff --git a/frontend/src/layouts/root-layout.tsx b/frontend/src/layouts/root-layout.tsx index d63a69e0..4eab8e34 100644 --- a/frontend/src/layouts/root-layout.tsx +++ b/frontend/src/layouts/root-layout.tsx @@ -26,7 +26,10 @@ export const RootLayout = () => { return ( <> - + {/* Show the auth modal when a `backgroundLocation` is set and when the user is not authenticated. */} +
{!pagesWithoutNavbarAndFooter.includes(pathname) && } From 54a9d0b4b0f22aea9eecf0f9f6e299d48227be01 Mon Sep 17 00:00:00 2001 From: jeafreezy Date: Tue, 25 Feb 2025 09:22:43 +0100 Subject: [PATCH 11/12] chore: adjusted root layout --- frontend/src/layouts/root-layout.tsx | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/frontend/src/layouts/root-layout.tsx b/frontend/src/layouts/root-layout.tsx index 4eab8e34..c3b65b5d 100644 --- a/frontend/src/layouts/root-layout.tsx +++ b/frontend/src/layouts/root-layout.tsx @@ -17,12 +17,7 @@ export const RootLayout = () => { scrollToTop(); }, [pathname]); const { isAuthenticated } = useAuth(); - const pagesWithoutNavbarAndFooter = [ - APPLICATION_ROUTES.AUTH_CALLBACK, - APPLICATION_ROUTES.START_MAPPING_BASE, - ]; - const pagesWithoutPadding = [APPLICATION_ROUTES.HOMEPAGE]; return ( <> @@ -31,17 +26,21 @@ export const RootLayout = () => { isOpen={state?.backgroundLocation && !isAuthenticated} />
- {!pagesWithoutNavbarAndFooter.includes(pathname) && } + {!pathname.includes(APPLICATION_ROUTES.AUTH_CALLBACK) && } - {!pagesWithoutNavbarAndFooter.includes(pathname) && } + {!pathname.includes(APPLICATION_ROUTES.AUTH_CALLBACK) && + !pathname.includes(APPLICATION_ROUTES.START_MAPPING_BASE) && ( + + )}
- {!pagesWithoutNavbarAndFooter.includes(pathname) &&
} + {!pathname.includes(APPLICATION_ROUTES.AUTH_CALLBACK) && + !pathname.includes(APPLICATION_ROUTES.AUTH_CALLBACK) &&
}
); From c91af6bac6f60790161453b00fbee48983f0cc65 Mon Sep 17 00:00:00 2001 From: Emmanuel Jolaiya Date: Tue, 25 Feb 2025 09:33:42 +0100 Subject: [PATCH 12/12] chore: fixed typo --- frontend/src/layouts/root-layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/layouts/root-layout.tsx b/frontend/src/layouts/root-layout.tsx index c3b65b5d..75c19e96 100644 --- a/frontend/src/layouts/root-layout.tsx +++ b/frontend/src/layouts/root-layout.tsx @@ -39,7 +39,7 @@ export const RootLayout = () => { >
- {!pathname.includes(APPLICATION_ROUTES.AUTH_CALLBACK) && + {!pathname.includes(APPLICATION_ROUTES.START_MAPPING_BASE) && !pathname.includes(APPLICATION_ROUTES.AUTH_CALLBACK) &&