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..9301a3d9 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", @@ -45,6 +47,7 @@ "@eslint/js": "^9.9.0", "@tailwindcss/typography": "^0.5.15", "@tanstack/eslint-plugin-query": "^5.58.1", + "@testing-library/react": "^16.2.0", "@types/geojson": "^7946.0.14", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", @@ -62,13 +65,15 @@ "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", "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..0d508e83 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': + 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 @@ -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 @@ -178,6 +184,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)(jsdom@26.0.0) packages: @@ -189,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'} @@ -280,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'} @@ -873,6 +913,25 @@ packages: '@terraformer/wkt@2.2.1': resolution: {integrity: sha512-XDUsW/lvbMzFi7GIuRD9+UqR4QyP+5C+TugeJLMDczKIRbaHoE9J3N8zLSdyOGmnJL9B6xTS3YMMlBnMU0Ar5A==} + '@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: + '@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 + '@types/react-dom': + optional: true + '@turf/area@7.1.0': resolution: {integrity: sha512-w91FEe02/mQfMPRX2pXua48scFuKJ2dSVMF2XmJ6+BJfFiCPxp95I3+Org8+ZsYv93CDNKbf0oLNEPnuQdgs2g==} @@ -903,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==} @@ -1088,6 +1150,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: @@ -1098,6 +1189,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==} @@ -1117,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'} @@ -1141,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'} @@ -1177,6 +1279,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 +1350,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 +1380,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 +1404,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'} @@ -1346,12 +1464,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'} @@ -1373,9 +1499,25 @@ 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 + + decimal.js@10.5.0: + resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==} + 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==} @@ -1411,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'} @@ -1430,6 +1575,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'} @@ -1446,6 +1595,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'} @@ -1454,6 +1606,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==} @@ -1581,10 +1737,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'} @@ -1661,6 +1824,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==} @@ -1809,12 +1976,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==} @@ -1947,6 +2130,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'} @@ -2019,6 +2205,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'} @@ -2151,12 +2346,22 @@ 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==} + 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==} + maplibre-gl@4.7.1: resolution: {integrity: sha512-lgL7XpIwsgICiL82ITplfS7IGwrB1OJIw/pCvprDp2dhmSSEBgmPzYRvwYYYvJGJD7fxUv1Tvpih4nZ6VrLuaA==} engines: {node: '>=16.14.0', npm: '>=8.1.0'} @@ -2355,6 +2560,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'} @@ -2413,6 +2621,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'} @@ -2428,6 +2639,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 @@ -2518,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==} @@ -2582,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: @@ -2704,6 +2929,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==} @@ -2722,6 +2950,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==} @@ -2776,6 +3011,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 +3044,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'} @@ -2882,6 +3126,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} @@ -2916,12 +3163,37 @@ 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'} + + 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'} @@ -2930,6 +3202,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==} @@ -3068,6 +3348,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,9 +3392,57 @@ 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==} + 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'} @@ -3136,6 +3469,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'} @@ -3148,10 +3486,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==} @@ -3176,6 +3533,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 @@ -3304,6 +3669,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': @@ -3778,6 +4163,27 @@ snapshots: '@terraformer/wkt@2.2.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-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.10 + '@types/react-dom': 18.3.0 + '@turf/area@7.1.0': dependencies: '@turf/helpers': 7.1.0 @@ -3848,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 @@ -4103,12 +4511,54 @@ 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 acorn@8.12.1: {} + agent-base@7.1.3: {} + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -4128,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: {} @@ -4149,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: {} @@ -4208,6 +4664,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 +4735,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 +4762,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 +4789,8 @@ snapshots: character-reference-invalid@2.0.1: {} + check-error@2.1.1: {} + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -4380,10 +4850,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 @@ -4406,10 +4886,18 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.4.0: + dependencies: + ms: 2.1.3 + + decimal.js@10.5.0: {} + 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: @@ -4442,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 @@ -4458,6 +4948,8 @@ snapshots: emoji-regex@9.2.2: {} + entities@4.5.0: {} + es-abstract@1.23.8: dependencies: array-buffer-byte-length: 1.0.2 @@ -4533,6 +5025,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 @@ -4543,6 +5037,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 @@ -4730,8 +5231,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 @@ -4806,6 +5313,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): @@ -4957,10 +5471,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: {} @@ -5078,6 +5614,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 @@ -5153,6 +5691,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: {} @@ -5323,12 +5889,20 @@ 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 + lz-string@1.5.0: {} + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + maplibre-gl@4.7.1: dependencies: '@mapbox/geojson-rewind': 0.5.2 @@ -5751,6 +6325,8 @@ snapshots: normalize-range@0.1.2: {} + nwsapi@2.2.16: {} + object-assign@4.1.1: {} object-hash@3.0.0: {} @@ -5828,6 +6404,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: {} @@ -5839,6 +6419,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 @@ -5912,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 @@ -5972,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 @@ -6158,6 +6750,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 @@ -6183,6 +6777,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 @@ -6252,6 +6852,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 +6879,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 @@ -6388,6 +6994,8 @@ snapshots: symbol-observable@1.2.0: {} + symbol-tree@3.2.4: {} + synckit@0.9.1: dependencies: '@pkgr/core': 0.1.1 @@ -6444,16 +7052,40 @@ 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: {} + + 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: {} @@ -6619,6 +7251,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,12 +7288,65 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + 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) + '@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 + jsdom: 26.0.0 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vt-pbf@3.1.3: dependencies: '@mapbox/point-geometry': 0.1.0 '@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 @@ -6692,6 +7395,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: @@ -6706,6 +7414,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 @@ -6713,6 +7425,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 bbf40b65..c9fef710 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 { 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, HOT_FAIR_LOCAL_STORAGE_ACCESS_TOKEN_KEY, HOT_FAIR_LOGIN_SUCCESSFUL_SESSION_KEY, HOT_FAIR_SESSION_REDIRECT_KEY, -} from "@/constants"; +} from "@/config"; +import { showErrorToast, showSuccessToast } from "@/utils"; +import { TUser } from "@/types/api"; +import { useLocalStorage, useSessionStorage } from "@/hooks/use-storage"; +import { APPLICATION_ROUTES, 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. */ @@ -136,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/providers/models-provider.tsx b/frontend/src/app/providers/models-provider.tsx index 7d47b7d6..a6c66ab5 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'; -import { BASE_MODELS, TrainingDatasetOption, TrainingType } from '@/enums'; -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'; + 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"; + import { TTrainingAreaFeature, TTrainingDataset, @@ -230,8 +231,8 @@ const ModelsContext = createContext<{ validateEditMode: boolean; }>({ formData: initialFormState, - setFormData: () => { }, - handleChange: () => { }, + setFormData: () => {}, + handleChange: () => {}, createNewTrainingDatasetMutation: {} as UseMutationResult< TTrainingDataset, Error, @@ -246,13 +247,13 @@ const ModelsContext = createContext<{ >, hasLabeledTrainingAreas: false, hasAOIsWithGeometry: false, - resetState: () => { }, + resetState: () => {}, isEditMode: false, modelId: "", getFullPath: () => "", - handleModelCreationAndUpdate: () => { }, + handleModelCreationAndUpdate: () => {}, trainingDatasetCreationInProgress: false, - handleTrainingDatasetCreation: () => { }, + handleTrainingDatasetCreation: () => {}, validateEditMode: false, }); @@ -262,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: @@ -282,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; }); }; @@ -361,7 +367,6 @@ export const ModelsProvider: React.FC<{ }; }, []); - const resetState = () => { removeSessionValue(HOT_FAIR_MODEL_CREATION_SESSION_STORAGE_KEY); setFormData(initialFormState); @@ -455,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 40e283ff..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, @@ -321,6 +321,19 @@ const router = createBrowserRouter([ * User account routes ends */ + /** + * Auth route + */ + { + path: APPLICATION_ROUTES.AUTH_CALLBACK, + lazy: async () => { + const { AuthenticationCallbackPage } = await import( + "@/app/routes/authenticate" + ); + return { Component: AuthenticationCallbackPage }; + }, + }, + /** * 404 route */ diff --git a/frontend/src/app/routes/authenticate.tsx b/frontend/src/app/routes/authenticate.tsx new file mode 100644 index 00000000..fa95fff0 --- /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/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 d75ef27f..9ba4c63a 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,15 @@ export const ProtectedRoute: React.FC = ({ children }) => { diff --git a/frontend/src/app/routes/resources.tsx b/frontend/src/app/routes/resources.tsx index 40b75214..1b78f4af 100644 --- a/frontend/src/app/routes/resources.tsx +++ b/frontend/src/app/routes/resources.tsx @@ -1,4 +1,4 @@ -import { PageUnderConstruction } from '@/components/errors'; +import { PageUnderConstruction } from "@/components/errors"; // import { ChevronDownIcon } from "@/components/ui/icons"; // import { FAQs, SectionHeader } from "@/components/shared"; // import { Head } from "@/components/seo"; @@ -9,7 +9,6 @@ import { PageUnderConstruction } from '@/components/errors'; // import { TArticle } from "@/types"; // import { truncateString } from "@/utils"; - export const ResourcesPage = () => { return ( diff --git a/frontend/src/app/routes/start-mapping.tsx b/frontend/src/app/routes/start-mapping.tsx index 8d84ea45..d02a2585 100644 --- a/frontend/src/app/routes/start-mapping.tsx +++ b/frontend/src/app/routes/start-mapping.tsx @@ -1,18 +1,22 @@ -import useScreenSize from '@/hooks/use-screen-size'; -import { APPLICATION_ROUTES } from '@/constants'; -import { BASE_MODELS } from '@/enums'; -import { FitToBounds, LayerControl, ZoomLevel } from '@/components/map'; -import { Head } from '@/components/seo'; -import { LngLatBoundsLike } from 'maplibre-gl'; -import { ModelDetailsPopUp } from '@/features/start-mapping/components'; -import { useCallback, useEffect, useState } from 'react'; -import { useDropdownMenu } from '@/hooks/use-dropdown-menu'; -import { useGetTMSTileJSON } from '@/features/model-creation/hooks/use-tms-tilejson'; -import { useGetTrainingDataset } from '@/features/models/hooks/use-dataset'; -import { useMapInstance } from '@/hooks/use-map-instance'; -import { useModelDetails } from '@/features/models/hooks/use-models'; -import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; -import { UserProfile } from '@/components/layout'; +import useScreenSize from "@/hooks/use-screen-size"; +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"; +import { LngLatBoundsLike } from "maplibre-gl"; +import { ModelDetailsPopUp } from "@/features/start-mapping/components"; +import { useCallback, useEffect, useState } from "react"; +import { useDropdownMenu } from "@/hooks/use-dropdown-menu"; +import { useGetTMSTileJSON } from "@/features/model-creation/hooks/use-tms-tilejson"; +import { useGetTrainingDataset } from "@/features/models/hooks/use-dataset"; +import { useMapInstance } from "@/hooks/use-map-instance"; +import { useModelDetails } from "@/features/models/hooks/use-models"; +import { useNavigate, useParams, useSearchParams } from "react-router-dom"; +import { UserProfile } from "@/components/layout"; import { BBOX, Feature, @@ -34,8 +38,6 @@ 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 +46,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; @@ -174,42 +176,42 @@ export const StartMappingPage = () => { const mapLayers = [ ...(modelPredictions.accepted.length > 0 ? [ - { - value: - START_MAPPING_PAGE_CONTENT.map.controls.legendControl - .acceptedPredictions, - subLayers: [ - ACCEPTED_MODEL_PREDICTIONS_FILL_LAYER_ID, - ACCEPTED_MODEL_PREDICTIONS_OUTLINE_LAYER_ID, - ], - }, - ] + { + value: + START_MAPPING_PAGE_CONTENT.map.controls.legendControl + .acceptedPredictions, + subLayers: [ + ACCEPTED_MODEL_PREDICTIONS_FILL_LAYER_ID, + ACCEPTED_MODEL_PREDICTIONS_OUTLINE_LAYER_ID, + ], + }, + ] : []), ...(modelPredictions.rejected.length > 0 ? [ - { - value: - START_MAPPING_PAGE_CONTENT.map.controls.legendControl - .rejectedPredictions, - subLayers: [ - REJECTED_MODEL_PREDICTIONS_FILL_LAYER_ID, - REJECTED_MODEL_PREDICTIONS_OUTLINE_LAYER_ID, - ], - }, - ] + { + value: + START_MAPPING_PAGE_CONTENT.map.controls.legendControl + .rejectedPredictions, + subLayers: [ + REJECTED_MODEL_PREDICTIONS_FILL_LAYER_ID, + REJECTED_MODEL_PREDICTIONS_OUTLINE_LAYER_ID, + ], + }, + ] : []), ...(modelPredictions.all.length > 0 ? [ - { - value: - START_MAPPING_PAGE_CONTENT.map.controls.legendControl - .predictionResults, - subLayers: [ - ALL_MODEL_PREDICTIONS_FILL_LAYER_ID, - ALL_MODEL_PREDICTIONS_OUTLINE_LAYER_ID, - ], - }, - ] + { + value: + START_MAPPING_PAGE_CONTENT.map.controls.legendControl + .predictionResults, + subLayers: [ + ALL_MODEL_PREDICTIONS_FILL_LAYER_ID, + ALL_MODEL_PREDICTIONS_OUTLINE_LAYER_ID, + ], + }, + ] : []), ]; 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 00000000..e11ed924 Binary files /dev/null and b/frontend/src/assets/images/osm_logo.png differ diff --git a/frontend/src/components/auth/auth-modal.tsx b/frontend/src/components/auth/auth-modal.tsx new file mode 100644 index 00000000..26b17b2e --- /dev/null +++ b/frontend/src/components/auth/auth-modal.tsx @@ -0,0 +1,64 @@ +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 { useNavigate } from "react-router-dom"; +import { Button } from "@/components/ui/button"; +import { AUTH_PAGE_AND_MODAL_CONTENT } from "@/constants/ui-contents/auth-content"; +import { Spinner } from "@/components/ui/spinner"; + +export const AuthenticationModal = ({ + callbackPage = false, + isOpen = false, +}: { + callbackPage?: boolean; + isOpen?: boolean; +}) => { + const { closeDialog } = useDialog(); + const navigate = useNavigate(); + const { handleLogin, loading } = useLogin(); + + const handleOnClose = () => { + closeDialog(); + navigate(-1); + }; + + return ( + +
+ +
+

+ {AUTH_PAGE_AND_MODAL_CONTENT.title} +

+

+ {callbackPage + ? AUTH_PAGE_AND_MODAL_CONTENT.authInProgressText + : AUTH_PAGE_AND_MODAL_CONTENT.instruction} +

+
+ {callbackPage ? ( + + ) : ( + + )} + +
+
+ ); +}; diff --git a/frontend/src/components/auth/index.ts b/frontend/src/components/auth/index.ts new file mode 100644 index 00000000..539ba16a --- /dev/null +++ b/frontend/src/components/auth/index.ts @@ -0,0 +1 @@ +export { AuthenticationModal } from "./auth-modal"; diff --git a/frontend/src/components/landing/kpi/kpi.tsx b/frontend/src/components/landing/kpi/kpi.tsx index 9cc263e0..0e30976e 100644 --- a/frontend/src/components/landing/kpi/kpi.tsx +++ b/frontend/src/components/landing/kpi/kpi.tsx @@ -1,7 +1,8 @@ -import styles from './kpi.module.css'; -import { API_ENDPOINTS, apiClient } from '@/services'; -import { KPI_STATS_CACHE_TIME_MS, 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; @@ -26,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/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..f5d80ab7 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,18 @@ export const NavBar = () => { {isAuthenticated ? ( ) : ( - )} @@ -62,12 +71,16 @@ export const NavBar = () => { )} diff --git a/frontend/src/components/layout/navbar/user-profile.tsx b/frontend/src/components/layout/navbar/user-profile.tsx index 082f8a8f..b52946d6 100644 --- a/frontend/src/components/layout/navbar/user-profile.tsx +++ b/frontend/src/components/layout/navbar/user-profile.tsx @@ -3,16 +3,13 @@ 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"; +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 c2dbba5e..f964e434 100644 --- a/frontend/src/components/map/controls/layer-control.tsx +++ b/frontend/src/components/map/controls/layer-control.tsx @@ -10,7 +10,7 @@ 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 }[]; diff --git a/frontend/src/components/map/layers/basemaps.tsx b/frontend/src/components/map/layers/basemaps.tsx index 87c4f007..511f5913 100644 --- a/frontend/src/components/map/layers/basemaps.tsx +++ b/frontend/src/components/map/layers/basemaps.tsx @@ -3,7 +3,7 @@ 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..95f8b9e9 100644 --- a/frontend/src/components/map/layers/open-aerial-map.tsx +++ b/frontend/src/components/map/layers/open-aerial-map.tsx @@ -1,5 +1,5 @@ import { Map } from "maplibre-gl"; -import { TMS_LAYER_ID, TMS_SOURCE_ID } from "@/constants"; +import { TMS_LAYER_ID, TMS_SOURCE_ID } from "@/config"; import { useMapLayers } from "@/hooks/use-map-layer"; export const OpenAerialMap = ({ diff --git a/frontend/src/components/map/layers/tile-boundaries.tsx b/frontend/src/components/map/layers/tile-boundaries.tsx index 742b2397..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 '@/constants'; -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 53e5745f..e83b70c5 100644 --- a/frontend/src/components/map/setups/setup-maplibre.ts +++ b/frontend/src/components/map/setups/setup-maplibre.ts @@ -1,6 +1,6 @@ import maplibregl, { Map } from "maplibre-gl"; import { BASEMAPS } from "@/enums"; -import { MAP_STYLES, MAX_ZOOM_LEVEL } from "@/constants"; +import { MAP_STYLES, MAX_ZOOM_LEVEL } from "@/config"; import { Protocol } from "pmtiles"; export const setupMaplibreMap = ( diff --git a/frontend/src/components/map/setups/setup-terra-draw.ts b/frontend/src/components/map/setups/setup-terra-draw.ts index fbc4e39a..3b5ab8b2 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 { 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..46a50012 100644 --- a/frontend/src/components/shared/hot-tracking.tsx +++ b/frontend/src/components/shared/hot-tracking.tsx @@ -1,6 +1,9 @@ import { APPLICATION_ROUTES } from "@/constants"; -import { ENVS } from "@/config/env"; -import { HOT_TRACKING_HTML_TAG_NAME } 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"; @@ -15,8 +18,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/components/shared/index.ts b/frontend/src/components/shared/index.ts index e4a50853..867c0d7a 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"; 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..f7e6fd31 --- /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} + +

+ ); +}; 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 new file mode 100644 index 00000000..7a32a469 --- /dev/null +++ b/frontend/src/config/__tests__/config.test.ts @@ -0,0 +1,62 @@ +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..20a60a37 100644 --- a/frontend/src/config/env.ts +++ b/frontend/src/config/env.ts @@ -1,265 +1,80 @@ /** - * 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". - */ - 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, - /** - 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. - */ - 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, - /** - The outline width for the training area AOI rectangles. - Data type: Positive Integer (e.g., 3). - Default value: 4. - */ - TRAINING_AREAS_AOI_OUTLINE_WIDTH: import.meta.env .VITE_TRAINING_AREAS_AOI_OUTLINE_WIDTH, - /** - 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_FILL_OPACITY: import.meta.env .VITE_TRAINING_AREAS_AOI_FILL_OPACITY, - /** - 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/. - */ - 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). - */ - 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, - /** - 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, - /** - 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, - /** - 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..81c9cde3 --- /dev/null +++ b/frontend/src/config/index.ts @@ -0,0 +1,391 @@ +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. + * 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, +); + +/** + * 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/routes.ts b/frontend/src/constants/routes.ts index 514aadd8..f32336e6 100644 --- a/frontend/src/constants/routes.ts +++ b/frontend/src/constants/routes.ts @@ -44,6 +44,7 @@ export const APPLICATION_ROUTES = { START_MAPPING_BASE: "/start-mapping/", START_MAPPING: "/start-mapping/:modelId", NOTFOUND: "/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 1fb22874..1e852fe2 100644 --- a/frontend/src/constants/ui-contents/models-content.ts +++ b/frontend/src/constants/ui-contents/models-content.ts @@ -1,7 +1,6 @@ -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 { TModelsContent } from "@/types"; export const MODELS_CONTENT: TModelsContent = { trainingArea: { @@ -31,11 +30,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.", }, @@ -67,13 +70,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", @@ -87,16 +92,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:", @@ -121,7 +129,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", @@ -138,8 +145,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: { @@ -154,7 +160,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", @@ -162,27 +169,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", @@ -320,8 +333,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 fd447ab6..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,19 +1,22 @@ 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 { DialogProps, Feature, FeatureCollection } from "@/types"; 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, - MODELS_CONTENT, -} from "@/constants"; + MIN_TRAINING_AREA_SIZE, +} from "@/config"; + import { formatAreaInAppropriateUnit, showErrorToast, @@ -306,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 2ec9197d..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,19 +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 { - 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, @@ -35,11 +31,7 @@ import { showWarningToast, truncateString, } from "@/utils"; -import { - MODELS_CONTENT, - TOAST_NOTIFICATIONS, - TRAINING_AREA_LABELS_FETCH_POOLING_TIME_MS, -} 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 51d3c49f..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 '@/constants'; -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 172f4842..ab20ef63 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/features/models/components/maps/training-area-map.tsx b/frontend/src/features/models/components/maps/training-area-map.tsx index 422c9b8c..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,4 +1,5 @@ import { ControlsPosition } from "@/enums"; +import { errorMessages } from "@/constants"; import { MapComponent } from "@/components/map"; import { PMTiles } from "pmtiles"; import { useCallback, useEffect, useRef, useState } from "react"; @@ -25,8 +26,7 @@ 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; diff --git a/frontend/src/features/models/components/model-details-properties.tsx b/frontend/src/features/models/components/model-details-properties.tsx index f2a6f0c3..a62b6f64 100644 --- a/frontend/src/features/models/components/model-details-properties.tsx +++ b/frontend/src/features/models/components/model-details-properties.tsx @@ -3,10 +3,10 @@ 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 { ENVS } from "@/config/env"; import { Image, ZoomableImage } from "@/components/ui/image"; import { Link } from "@/components/ui/link"; import { ModelFilesDialog } from "./dialogs"; @@ -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/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 (
{ - 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..88a5aab4 --- /dev/null +++ b/frontend/src/hooks/__tests__/use-login.test.ts @@ -0,0 +1,83 @@ +// @ts-nocheck +import { act, renderHook } from "@testing-library/react"; +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 ce8f41cf..6eadf993 100644 --- a/frontend/src/hooks/use-layer-order.ts +++ b/frontend/src/hooks/use-layer-order.ts @@ -1,5 +1,5 @@ import { Map } from "maplibre-gl"; -import { TILE_BOUNDARY_LAYER_ID, TMS_LAYER_ID } from "@/constants"; +import { TILE_BOUNDARY_LAYER_ID, TMS_LAYER_ID } from "@/config"; import { useEffect, useRef } from "react"; type UseLayerReorderProps = { diff --git a/frontend/src/hooks/use-login.ts b/frontend/src/hooks/use-login.ts index 88a8a799..af70a09f 100644 --- a/frontend/src/hooks/use-login.ts +++ b/frontend/src/hooks/use-login.ts @@ -1,12 +1,11 @@ -import { authService } from '@/services'; -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"; +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. * @@ -22,13 +21,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/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 16484995..75c19e96 100644 --- a/frontend/src/layouts/root-layout.tsx +++ b/frontend/src/layouts/root-layout.tsx @@ -6,23 +6,32 @@ 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 { useAuth } from "@/app/providers/auth-provider"; +import { AuthenticationModal } 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 { isAuthenticated } = useAuth(); return ( <> + {/* Show the auth modal when a `backgroundLocation` is set and when the user is not authenticated. */} +
- - {!pathname.includes(APPLICATION_ROUTES.START_MAPPING_BASE) && ( - - )} + {!pathname.includes(APPLICATION_ROUTES.AUTH_CALLBACK) && } + + {!pathname.includes(APPLICATION_ROUTES.AUTH_CALLBACK) && + !pathname.includes(APPLICATION_ROUTES.START_MAPPING_BASE) && ( + + )}
{ >
- {!pathname.includes(APPLICATION_ROUTES.START_MAPPING_BASE) && ( -
- )} + {!pathname.includes(APPLICATION_ROUTES.START_MAPPING_BASE) && + !pathname.includes(APPLICATION_ROUTES.AUTH_CALLBACK) &&
}
); diff --git a/frontend/src/services/__tests__/auth.test.ts b/frontend/src/services/__tests__/auth.test.ts new file mode 100644 index 00000000..475e7af9 --- /dev/null +++ b/frontend/src/services/__tests__/auth.test.ts @@ -0,0 +1,122 @@ +// @ts-nocheck + +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 0e2493c4..53a8676b 100644 --- a/frontend/src/services/api-client.ts +++ b/frontend/src/services/api-client.ts @@ -1,8 +1,10 @@ import Axios, { InternalAxiosRequestConfig } from "axios"; -import { ENVS } from "@/config/env"; -import { HOT_FAIR_LOCAL_STORAGE_ACCESS_TOKEN_KEY } from "@/constants"; +import { + BASE_API_URL, + HOT_FAIR_LOCAL_STORAGE_ACCESS_TOKEN_KEY, +} from "@/config"; import { showErrorToast } from "@/utils"; -export const BASE_API_URL = ENVS.BASE_API_URL; + /** * The global axios API client. */ diff --git a/frontend/src/services/api-routes.ts b/frontend/src/services/api-routes.ts index 92efbaa9..3000eab3 100644 --- a/frontend/src/services/api-routes.ts +++ b/frontend/src/services/api-routes.ts @@ -1,4 +1,7 @@ -import { ENVS } from '@/config/env'; +import { + FAIR_PREDICTOR_API_ENDPOINT, + OSM_DATABASE_STATUS_API_ENDPOINT, +} from "@/config"; /** * The backend API endpoints. @@ -12,11 +15,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/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 df9cdb13..13e62653 100644 --- a/frontend/src/utils/geo/geo-utils.ts +++ b/frontend/src/utils/geo/geo-utils.ts @@ -1,16 +1,17 @@ -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 bbox from "@turf/bbox"; import { + BASE_API_URL, JOSM_REMOTE_URL, MAX_TRAINING_AREA_SIZE, MIN_TRAINING_AREA_SIZE, OSM_HASHTAGS, - TOAST_NOTIFICATIONS, -} from "@/constants"; +} 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 @@ -125,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 f5d9018a..03134cd4 100644 --- a/frontend/src/utils/geo/geometry-utils.ts +++ b/frontend/src/utils/geo/geometry-utils.ts @@ -36,12 +36,19 @@ 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/number-utils.ts b/frontend/src/utils/number-utils.ts index df32f4d8..26e7e5ed 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 (typeof num !== "number" || isNaN(num)) { + return 0; + } return Number(num.toFixed(round)); }; 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 0e6f11a7..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 '@/constants'; +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..a2a54d09 100644 --- a/frontend/vite.config.mts +++ b/frontend/vite.config.mts @@ -1,13 +1,23 @@ -import { defineConfig } from "vite"; +import { defineConfig } from "vitest/config"; 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'], + } });