diff --git a/frontend/.env.sample b/frontend/.env.sample index 1c535640..75862b77 100644 --- a/frontend/.env.sample +++ b/frontend/.env.sample @@ -1,46 +1,117 @@ -VITE_BASE_API_URL = 'http://localhost:8000/api/v1/' # backend api url +# 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. +VITE_BASE_API_URL = 'http://localhost:8000/api/v1/' + +# The matomo application ID. +# Data type: Positive Integer (e.g., 0). +# Default value: 0. 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" -# The maximum allowed area size (in square meters) for training areas. -# Data type: Positive Integer e.g 500000 -MAX_TRAINING_AREA_SIZE = +# 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. +VITE_KPI_STATS_CACHE_TIME = 900 -# The minimum allowed area size (in square meters) for training areas. -# Data type: Positive Integer e.g 500000 -MIN_TRAINING_AREA_SIZE = +# 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). +VITE_MAX_TRAINING_AREA_SIZE = 5000000 -# The maximum file size (in bytes) allowed for training area upload. -# Data type: Positive Integer e.g 500000 -MAX_TRAINING_AREA_UPLOAD_FILE_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. +VITE_MIN_TRAINING_AREA_SIZE = 5797 + +# 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). +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' -FAIR_VERSION = - -# Comma separated hashtags to add to the OSM ID Editor redirection -# Data type: String e.g '#HOT-fAIr, #AI-Assited-Mapping' -OSM_HASHTAGS = +# 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 = # The maximum zoom level for the map. -# Data type: Positive Integer e.g 22. Must be between 0 - 24 -MAX_ZOOM_LEVEL = - -# The minimum zoom level to show the training area labels. -# Data type: Positive Integer e.g 18. Must be between 0 - 24 -TRAINING_LABELS_MIN_ZOOM_LEVEL = - -# Training area and labels styles. -# Opacities are between 0 and 1 . E.g 0.5 -# Widths must be Positive Integers e.g 1, 2 etc. -# Colors must be hex codes or valid colors. E.g 'red', 'green', '#fff' -TRAINING_AREAS_AOI_FILL_COLOR = -TRAINING_AREAS_AOI_OUTLINE_COLOR = -TRAINING_AREAS_AOI_OUTLINE_WIDTH = -TRAINING_AREAS_AOI_FILL_OPACITY = -TRAINING_AREAS_AOI_LABELS_FILL_OPACITY = -TRAINING_AREAS_AOI_LABELS_OUTLINE_WIDTH = -TRAINING_AREAS_AOI_LABELS_FILL_COLOR = -TRAINING_AREAS_AOI_LABELS_OUTLINE_COLOR = \ No newline at end of file +# Data type: Positive Integer (e.g., 22). +# Note: Value must be between 0 - 24. +# Default value: 22. +VITE_MAX_ZOOM_LEVEL = 22 + +# 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. +VITE_MIN_ZOOM_LEVEL_FOR_START_MAPPING_PREDICTION = 19 + +# 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. +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. +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. +VITE_TRAINING_AREAS_AOI_OUTLINE_COLOR = "#247DCACC" + +# The outline width for the training area AOI rectangles. +# Data type: Positive Integer (e.g., 3). +# Default value: 4. +VITE_TRAINING_AREAS_AOI_OUTLINE_WIDTH = 4 + +# 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. +VITE_TRAINING_AREAS_AOI_FILL_OPACITY = 0.4 + +# 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. +VITE_TRAINING_AREAS_AOI_LABELS_FILL_OPACITY = 0.3 + +# The outline width for the training area AOI labels. +# Data type: Positive Integer (e.g., 3). +# Default value: 2. +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. +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. +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/. +VITE_JOSM_REMOTE_URL = "http://127.0.0.1:8111/" \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 72da6a36..277d3edc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,11 +17,13 @@ "@terraformer/wkt": "^2.2.1", "@turf/area": "^7.1.0", "@turf/bbox": "^7.1.0", + "@turf/boolean-intersects": "^7.1.0", "axios": "^1.7.7", "clsx": "^2.1.1", "framer-motion": "^11.5.4", "geojson": "^0.5.0", "maplibre-gl": "^4.7.1", + "pmtiles": "^4.1.0", "react": "^18.3.1", "react-confetti-explosion": "^2.1.2", "react-dom": "^18.3.1", @@ -29,10 +31,12 @@ "react-error-boundary": "^4.0.13", "react-helmet-async": "^2.0.5", "react-markdown": "^9.0.1", + "react-medium-image-zoom": "^5.2.11", "react-router-dom": "^6.26.2", "remark-gfm": "^4.0.0", "tailwind-merge": "^2.5.2", - "terra-draw": "1.0.0-beta.8" + "terra-draw": "1.0.0-beta.8", + "xmlbuilder2": "^3.1.1" }, "devDependencies": { "@eslint/js": "^9.9.0", @@ -57,5 +61,11 @@ "typescript-eslint": "^8.0.1", "vite": "^5.4.1", "vite-tsconfig-paths": "^5.0.1" + }, + "pnpm": { + "overrides": { + "cross-spawn@>=7.0.0 <7.0.5": ">=7.0.5", + "@eslint/plugin-kit@<0.2.3": ">=0.2.3" + } } } diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 1485ac06..a46a7f0a 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -4,6 +4,10 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + cross-spawn@>=7.0.0 <7.0.5: ">=7.0.5" + "@eslint/plugin-kit@<0.2.3": ">=0.2.3" + importers: .: dependencies: @@ -28,6 +32,9 @@ importers: "@turf/bbox": specifier: ^7.1.0 version: 7.1.0 + "@turf/boolean-intersects": + specifier: ^7.1.0 + version: 7.1.0 axios: specifier: ^1.7.7 version: 1.7.7 @@ -43,6 +50,9 @@ importers: maplibre-gl: specifier: ^4.7.1 version: 4.7.1 + pmtiles: + specifier: ^4.1.0 + version: 4.1.0 react: specifier: ^18.3.1 version: 18.3.1 @@ -64,6 +74,9 @@ importers: react-markdown: specifier: ^9.0.1 version: 9.0.1(@types/react@18.3.10)(react@18.3.1) + react-medium-image-zoom: + specifier: ^5.2.11 + version: 5.2.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-router-dom: specifier: ^6.26.2 version: 6.26.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -76,6 +89,9 @@ importers: terra-draw: specifier: 1.0.0-beta.8 version: 1.0.0-beta.8 + xmlbuilder2: + specifier: ^3.1.1 + version: 3.1.1 devDependencies: "@eslint/js": specifier: ^9.9.0 @@ -590,10 +606,10 @@ packages: } engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - "@eslint/plugin-kit@0.2.0": + "@eslint/plugin-kit@0.2.3": resolution: { - integrity: sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==, + integrity: sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==, } engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } @@ -762,6 +778,34 @@ packages: } engines: { node: ">= 8" } + "@oozcitak/dom@1.15.10": + resolution: + { + integrity: sha512-0JT29/LaxVgRcGKvHmSrUTEvZ8BXvZhGl2LASRUgHqDTC1M5g1pLmVv56IYNyt3bG2CUjDkc67wnyZC14pbQrQ==, + } + engines: { node: ">=8.0" } + + "@oozcitak/infra@1.0.8": + resolution: + { + integrity: sha512-JRAUc9VR6IGHOL7OGF+yrvs0LO8SlqGnPAMqyzOuFZPSZSXI7Xf2O9+awQPSMXgIWGtgUf/dA6Hs6X6ySEaWTg==, + } + engines: { node: ">=6.0" } + + "@oozcitak/url@1.0.4": + resolution: + { + integrity: sha512-kDcD8y+y3FCSOvnBI6HJgl00viO/nGbQoCINmQ0h98OhnGITrWR3bOGfwYCthgcrV8AnTJz8MzslTQbC3SOAmw==, + } + engines: { node: ">=8.0" } + + "@oozcitak/util@8.3.8": + resolution: + { + integrity: sha512-T8TbSnGsxo6TDBJx/Sgv/BlVJL3tshxZP7Aq5R1mSnM5OcHY2dQaxLMu2+E8u3gN0MLOzdjurqN4ZRVuzQycOQ==, + } + engines: { node: ">=8.0" } + "@pkgjs/parseargs@0.11.0": resolution: { @@ -1010,18 +1054,54 @@ packages: integrity: sha512-PdWPz9tW86PD78vSZj2fiRaB8JhUHy6piSa/QXb83lucxPK+HTAdzlDQMTKj5okRCU8Ox/25IR2ep9T8NdopRA==, } + "@turf/boolean-disjoint@7.1.0": + resolution: + { + integrity: sha512-JapOG03kOCoGeYMWgTQjEifhr1nUoK4Os2cX0iC5X9kvZF4qCHeruX8/rffBQDx7PDKQKusSTXq8B1ISFi0hOw==, + } + + "@turf/boolean-intersects@7.1.0": + resolution: + { + integrity: sha512-gpksWbb0RT+Z3nfqRfoACY3KEFyv2BPaxJ3L76PH67DhHZviq3Nfg85KYbpuhS64FSm+9tXe4IaKn6EjbHo20g==, + } + + "@turf/boolean-point-in-polygon@7.1.0": + resolution: + { + integrity: sha512-mprVsyIQ+ijWTZwbnO4Jhxu94ZW2M2CheqLiRTsGJy0Ooay9v6Av5/Nl3/Gst7ZVXxPqMeMaFYkSzcTc87AKew==, + } + "@turf/helpers@7.1.0": resolution: { integrity: sha512-dTeILEUVeNbaEeoZUOhxH5auv7WWlOShbx7QSd4s0T4Z0/iz90z9yaVCtZOLbU89umKotwKaJQltBNO9CzVgaQ==, } + "@turf/invariant@7.1.0": + resolution: + { + integrity: sha512-OCLNqkItBYIP1nE9lJGuIUatWGtQ4rhBKAyTfFu0z8npVzGEYzvguEeof8/6LkKmTTEHW53tCjoEhSSzdRh08Q==, + } + + "@turf/line-intersect@7.1.0": + resolution: + { + integrity: sha512-JI3dvOsAoCqd4vUJ134FIzgcC42QpC/tBs+b4OJoxWmwDek3REv4qGaZY6wCg9X4hFSlCKFcnhMIQQZ/n720Qg==, + } + "@turf/meta@7.1.0": resolution: { integrity: sha512-ZgGpWWiKz797Fe8lfRj7HKCkGR+nSJ/5aKXMyofCvLSc2PuYJs/qyyifDPWjASQQCzseJ7AlF2Pc/XQ/3XkkuA==, } + "@turf/polygon-to-line@7.1.0": + resolution: + { + integrity: sha512-FBlfyBWNQZCTVGqlJH7LR2VXmvj8AydxrA8zegqek/5oPGtQDeUgIppKmvmuNClqbglhv59QtCUVaDK4bOuCTA==, + } + "@types/babel__core@7.20.5": resolution: { @@ -1338,6 +1418,12 @@ packages: integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==, } + argparse@1.0.10: + resolution: + { + integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==, + } + argparse@2.0.1: resolution: { @@ -1586,10 +1672,10 @@ packages: integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==, } - cross-spawn@7.0.3: + cross-spawn@7.0.6: resolution: { - integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==, + integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==, } engines: { node: ">= 8" } @@ -1825,6 +1911,14 @@ packages: } engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + esprima@4.0.1: + resolution: + { + integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==, + } + engines: { node: ">=4" } + hasBin: true + esquery@1.6.0: resolution: { @@ -1916,6 +2010,12 @@ packages: integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==, } + fflate@0.8.2: + resolution: + { + integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==, + } + file-entry-cache@8.0.0: resolution: { @@ -2364,6 +2464,13 @@ packages: integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==, } + js-yaml@3.14.1: + resolution: + { + integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==, + } + hasBin: true + js-yaml@4.1.0: resolution: { @@ -3107,6 +3214,18 @@ packages: } engines: { node: ">= 6" } + pmtiles@4.1.0: + resolution: + { + integrity: sha512-wymKYI61y3yI/iTzYHW18l6ViYBnF7TXbFnXb9q7RcWqkQ2EfgQVDzEIc5lImd3aAVkxu2tuWaoYhokov6N9VA==, + } + + point-in-polygon-hao@1.1.0: + resolution: + { + integrity: sha512-3hTIM2j/v9Lio+wOyur3kckD4NxruZhpowUbEgmyikW+a2Kppjtu1eN+AhnMQtoHW46zld88JiYWv6fxpsDrTQ==, + } + postcss-import@15.1.0: resolution: { @@ -3336,6 +3455,15 @@ packages: "@types/react": ">=18" react: ">=18" + react-medium-image-zoom@5.2.11: + resolution: + { + integrity: sha512-K3REdn96k2H+6iQlRSl7C7O5lMhdhRx3W1NFJXRar6wMeHpOwp5wI/6N0SfuF/NiKu+HIPxY0FSdvMIJwynTCw==, + } + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-refresh@0.14.2: resolution: { @@ -3561,6 +3689,12 @@ packages: } engines: { node: ">=0.10.0" } + sprintf-js@1.0.3: + resolution: + { + integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==, + } + string-width@4.2.3: resolution: { @@ -3643,6 +3777,12 @@ packages: } engines: { node: ">= 0.4" } + sweepline-intersections@1.5.0: + resolution: + { + integrity: sha512-AoVmx72QHpKtItPu72TzFL+kcYjd67BPLDoR0LarIk+xyaRg+pDTMFXndIEvZf9xEKnJv6JdhgRMnocoG0D3AQ==, + } + symbol-observable@1.2.0: resolution: { @@ -3711,6 +3851,12 @@ packages: integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==, } + tinyqueue@2.0.3: + resolution: + { + integrity: sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==, + } + tinyqueue@3.0.0: resolution: { @@ -3980,6 +4126,13 @@ packages: } engines: { node: ">=12" } + xmlbuilder2@3.1.1: + resolution: + { + integrity: sha512-WCSfbfZnQDdLQLiMdGUQpMxxckeQ4oZNMNhLVkcekTu7xhD4tuUDyAPoY8CwXvBYE6LwBHd6QW2WZXlOWr1vCw==, + } + engines: { node: ">=12.0" } + yallist@3.1.1: resolution: { @@ -4255,7 +4408,7 @@ snapshots: "@eslint/object-schema@2.1.4": {} - "@eslint/plugin-kit@0.2.0": + "@eslint/plugin-kit@0.2.3": dependencies: levn: 0.4.1 @@ -4352,6 +4505,23 @@ snapshots: "@nodelib/fs.scandir": 2.1.5 fastq: 1.17.1 + "@oozcitak/dom@1.15.10": + dependencies: + "@oozcitak/infra": 1.0.8 + "@oozcitak/url": 1.0.4 + "@oozcitak/util": 8.3.8 + + "@oozcitak/infra@1.0.8": + dependencies: + "@oozcitak/util": 8.3.8 + + "@oozcitak/url@1.0.4": + dependencies: + "@oozcitak/infra": 1.0.8 + "@oozcitak/util": 8.3.8 + + "@oozcitak/util@8.3.8": {} + "@pkgjs/parseargs@0.11.0": optional: true @@ -4479,16 +4649,62 @@ snapshots: "@types/geojson": 7946.0.14 tslib: 2.7.0 + "@turf/boolean-disjoint@7.1.0": + dependencies: + "@turf/boolean-point-in-polygon": 7.1.0 + "@turf/helpers": 7.1.0 + "@turf/line-intersect": 7.1.0 + "@turf/meta": 7.1.0 + "@turf/polygon-to-line": 7.1.0 + "@types/geojson": 7946.0.14 + tslib: 2.7.0 + + "@turf/boolean-intersects@7.1.0": + dependencies: + "@turf/boolean-disjoint": 7.1.0 + "@turf/helpers": 7.1.0 + "@turf/meta": 7.1.0 + "@types/geojson": 7946.0.14 + tslib: 2.7.0 + + "@turf/boolean-point-in-polygon@7.1.0": + dependencies: + "@turf/helpers": 7.1.0 + "@turf/invariant": 7.1.0 + "@types/geojson": 7946.0.14 + point-in-polygon-hao: 1.1.0 + tslib: 2.7.0 + "@turf/helpers@7.1.0": dependencies: "@types/geojson": 7946.0.14 tslib: 2.7.0 + "@turf/invariant@7.1.0": + dependencies: + "@turf/helpers": 7.1.0 + "@types/geojson": 7946.0.14 + tslib: 2.7.0 + + "@turf/line-intersect@7.1.0": + dependencies: + "@turf/helpers": 7.1.0 + "@types/geojson": 7946.0.14 + sweepline-intersections: 1.5.0 + tslib: 2.7.0 + "@turf/meta@7.1.0": dependencies: "@turf/helpers": 7.1.0 "@types/geojson": 7946.0.14 + "@turf/polygon-to-line@7.1.0": + dependencies: + "@turf/helpers": 7.1.0 + "@turf/invariant": 7.1.0 + "@types/geojson": 7946.0.14 + tslib: 2.7.0 + "@types/babel__core@7.20.5": dependencies: "@babel/parser": 7.25.6 @@ -4703,6 +4919,10 @@ snapshots: arg@5.0.2: {} + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + argparse@2.0.1: {} arr-union@3.1.0: {} @@ -4833,7 +5053,7 @@ snapshots: convert-source-map@2.0.0: {} - cross-spawn@7.0.3: + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 shebang-command: 2.0.0 @@ -4958,7 +5178,7 @@ snapshots: "@eslint/core": 0.6.0 "@eslint/eslintrc": 3.1.0 "@eslint/js": 9.11.1 - "@eslint/plugin-kit": 0.2.0 + "@eslint/plugin-kit": 0.2.3 "@humanwhocodes/module-importer": 1.0.1 "@humanwhocodes/retry": 0.3.0 "@nodelib/fs.walk": 1.2.8 @@ -4966,7 +5186,7 @@ snapshots: "@types/json-schema": 7.0.15 ajv: 6.12.6 chalk: 4.1.2 - cross-spawn: 7.0.3 + cross-spawn: 7.0.6 debug: 4.3.7 escape-string-regexp: 4.0.0 eslint-scope: 8.1.0 @@ -5000,6 +5220,8 @@ snapshots: acorn-jsx: 5.3.2(acorn@8.12.1) eslint-visitor-keys: 4.1.0 + esprima@4.0.1: {} + esquery@1.6.0: dependencies: estraverse: 5.3.0 @@ -5045,6 +5267,8 @@ snapshots: dependencies: reusify: 1.0.4 + fflate@0.8.2: {} + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -5073,7 +5297,7 @@ snapshots: foreground-child@3.3.0: dependencies: - cross-spawn: 7.0.3 + cross-spawn: 7.0.6 signal-exit: 4.1.0 form-data@4.0.0: @@ -5262,6 +5486,11 @@ snapshots: js-tokens@4.0.0: {} + js-yaml@3.14.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + js-yaml@4.1.0: dependencies: argparse: 2.0.1 @@ -5911,6 +6140,12 @@ snapshots: pirates@4.0.6: {} + pmtiles@4.1.0: + dependencies: + fflate: 0.8.2 + + point-in-polygon-hao@1.1.0: {} + postcss-import@15.1.0(postcss@8.4.47): dependencies: postcss: 8.4.47 @@ -6055,6 +6290,11 @@ snapshots: transitivePeerDependencies: - supports-color + react-medium-image-zoom@5.2.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-refresh@0.14.2: {} react-router-dom@6.26.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): @@ -6207,6 +6447,8 @@ snapshots: dependencies: extend-shallow: 3.0.2 + sprintf-js@1.0.3: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -6262,6 +6504,10 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + sweepline-intersections@1.5.0: + dependencies: + tinyqueue: 2.0.3 + symbol-observable@1.2.0: {} synckit@0.9.1: @@ -6320,6 +6566,8 @@ snapshots: tiny-warning@1.0.3: {} + tinyqueue@2.0.3: {} + tinyqueue@3.0.0: {} to-fast-properties@2.0.0: {} @@ -6476,6 +6724,13 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.1.0 + xmlbuilder2@3.1.1: + dependencies: + "@oozcitak/dom": 1.15.10 + "@oozcitak/infra": 1.0.8 + "@oozcitak/util": 8.3.8 + js-yaml: 3.14.1 + yallist@3.1.1: {} yaml@2.5.1: {} diff --git a/frontend/src/app/index.tsx b/frontend/src/app/index.tsx index 9f48e1d1..5a1245e2 100644 --- a/frontend/src/app/index.tsx +++ b/frontend/src/app/index.tsx @@ -7,23 +7,22 @@ import { QueryClientProvider, } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; -import { showErrorToast } from "@/utils"; +import { HOT_TRACKING_HTML_TAG_NAME, showErrorToast } from "@/utils"; export const App = () => { - const hotTrackingTagName = "hot-tracking"; - const setupHotTracking = () => { - const hotTracking = document.createElement(hotTrackingTagName); - //adding a class for easy configuration in the css + const hotTracking = document.createElement(HOT_TRACKING_HTML_TAG_NAME); + // adding a css class to style the component in the `styles/index.css` file. hotTracking.classList.add("hot-matomo"); - // setting the other attributes + // setting the other attributes. hotTracking.setAttribute("site-id", ENVS.MATOMO_ID); hotTracking.setAttribute("domain", ENVS.MATOMO_APP_DOMAIN); hotTracking.setAttribute("force", "true"); document.body.appendChild(hotTracking); }; useEffect(() => { - if (document.getElementsByTagName(hotTrackingTagName).length > 0) return; + if (document.getElementsByTagName(HOT_TRACKING_HTML_TAG_NAME).length > 0) + return; setupHotTracking(); return; }, []); diff --git a/frontend/src/app/providers/auth-provider.tsx b/frontend/src/app/providers/auth-provider.tsx index 1fa881da..6b89013c 100644 --- a/frontend/src/app/providers/auth-provider.tsx +++ b/frontend/src/app/providers/auth-provider.tsx @@ -8,8 +8,8 @@ import { HOT_FAIR_SESSION_REDIRECT_KEY, showErrorToast, showSuccessToast, - TOAST_NOTIFICATIONS, } from "@/utils"; +import { TOAST_NOTIFICATIONS } from "@/constants"; import React, { createContext, useContext, useState, useEffect } from "react"; type TAuthContext = { @@ -23,7 +23,13 @@ type TAuthContext = { // @ts-expect-error bad type definition const AuthContext = createContext(null); -export const useAuth = () => useContext(AuthContext); +export const useAuth = () => { + const context = useContext(AuthContext); + if (!context) { + throw new Error("useAuth must be used within an AuthProvider"); + } + return context; +}; type AuthProviderProps = { children: React.ReactNode; diff --git a/frontend/src/app/providers/index.tsx b/frontend/src/app/providers/index.tsx index d0bbe6ac..15954b76 100644 --- a/frontend/src/app/providers/index.tsx +++ b/frontend/src/app/providers/index.tsx @@ -1,5 +1,5 @@ import { HelmetProvider } from "react-helmet-async"; -import { AuthProvider } from "./auth-provider"; +import { AuthProvider } from "@/app/providers/auth-provider"; const ContextProviders = ({ children }: { children: React.ReactNode }) => { return ( diff --git a/frontend/src/app/providers/map-provider.tsx b/frontend/src/app/providers/map-provider.tsx deleted file mode 100644 index 16f934fe..00000000 --- a/frontend/src/app/providers/map-provider.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { - createContext, - useContext, - ReactNode, - useState, - useMemo, - useEffect, - useCallback, -} from "react"; -import { Map } from "maplibre-gl"; -import { TerraDraw } from "terra-draw"; -import { setupTerraDraw } from "@/components/map/setup-terra-draw"; -import { DrawingModes } from "@/enums"; - -const MapContext = createContext<{ - map: Map | null; - setMap: React.Dispatch>; - terraDraw: TerraDraw | undefined; - drawingMode: DrawingModes; - setDrawingMode: React.Dispatch>; - currentZoom: number; -}>({ - map: null, - setMap: () => {}, - terraDraw: undefined, - drawingMode: DrawingModes.STATIC, - setDrawingMode: () => DrawingModes, - currentZoom: 0, -}); - -export const MapProvider = ({ children }: { children: ReactNode }) => { - const [map, setMap] = useState(null); - const [currentZoom, setCurrentZoom] = useState(0); - const terraDraw = useMemo(() => { - if (map) { - const terraDraw = setupTerraDraw(map); - terraDraw.start(); - return terraDraw; - } - }, [map]); - - const [drawingMode, setDrawingMode] = useState( - DrawingModes.STATIC, - ); - - // sync the modes - useEffect(() => { - terraDraw?.setMode(drawingMode); - }, [terraDraw, drawingMode]); - - const updateZoom = useCallback(() => { - if (!map) return; - setCurrentZoom(map.getZoom()); - }, [map]); - - useEffect(() => { - if (!map) return; - const handleMapMove = () => { - updateZoom(); - }; - map.on("moveend", handleMapMove); - return () => { - map.off("moveend", handleMapMove); - }; - }, [map]); - - return ( - - {children} - - ); -}; - -export const useMap = () => useContext(MapContext); diff --git a/frontend/src/app/providers/models-provider.tsx b/frontend/src/app/providers/models-provider.tsx index 1ae1b717..6e7ae18f 100644 --- a/frontend/src/app/providers/models-provider.tsx +++ b/frontend/src/app/providers/models-provider.tsx @@ -7,7 +7,6 @@ import { showErrorToast, showSuccessToast, TMS_URL_REGEX_PATTERN, - TOAST_NOTIFICATIONS, } from "@/utils"; import { UseMutationResult } from "@tanstack/react-query"; import React, { @@ -33,6 +32,8 @@ import { LngLatBoundsLike } from "maplibre-gl"; import { useModelDetails } from "@/features/models/hooks/use-models"; import { useGetTrainingDataset } from "@/features/models/hooks/use-dataset"; +import { TOAST_NOTIFICATIONS } from "@/constants"; + /** * The names here are the same with the `initialFormState` object keys. * They are also the same with the form validation configuration object keys. diff --git a/frontend/src/app/router.tsx b/frontend/src/app/router.tsx index 38ce807c..f464f707 100644 --- a/frontend/src/app/router.tsx +++ b/frontend/src/app/router.tsx @@ -8,13 +8,14 @@ import { import { ProtectedPage } from "@/app/routes/protected-route"; import { MainErrorFallback } from "@/components/errors"; import ModelFormsLayout from "@/components/layouts/model-forms-layout"; -import ModelsLayout from "@/components/layouts/models-layout"; -import { MapProvider } from "./providers/map-provider"; const router = createBrowserRouter([ { element: , children: [ + /** + * Landing page route starts + */ { path: APPLICATION_ROUTES.HOMEPAGE, lazy: async () => { @@ -22,6 +23,9 @@ const router = createBrowserRouter([ return { Component: LandingPage }; }, }, + /** + * Landing page route ends + */ { path: APPLICATION_ROUTES.LEARN, lazy: async () => { @@ -43,6 +47,9 @@ const router = createBrowserRouter([ return { Component: ResourcesPage }; }, }, + /** + * Training dataset route starts + */ { path: APPLICATION_ROUTES.TRAINING_DATASETS, lazy: async () => { @@ -52,33 +59,37 @@ const router = createBrowserRouter([ return { Component: TrainingDatasetsPage }; }, }, + /** + * Training dataset route ends + */ + /** + * Models details & list route starts + */ { - element: , - children: [ - { - path: APPLICATION_ROUTES.MODEL_DETAILS, - lazy: async () => { - const { ModelDetailsPage } = await import( - "@/app/routes/models/model-details" - ); - return { - Component: () => , - }; - }, - }, - { - path: APPLICATION_ROUTES.MODELS, - lazy: async () => { - const { ModelsPage } = await import( - "@/app/routes/models/models-list" - ); - return { - Component: () => , - }; - }, - }, - ], + path: APPLICATION_ROUTES.MODEL_DETAILS, + lazy: async () => { + const { ModelDetailsPage } = await import( + "@/app/routes/models/model-details" + ); + return { + Component: () => , + }; + }, + }, + { + path: APPLICATION_ROUTES.MODELS, + lazy: async () => { + const { ModelsPage } = await import( + "@/app/routes/models/models-list" + ); + return { + Component: () => , + }; + }, }, + /** + * Models details & list route ends + */ { element: ( @@ -86,7 +97,9 @@ const router = createBrowserRouter([ ), children: [ - // Creation + /** + * Model creation routes + */ { path: APPLICATION_ROUTES.CREATE_NEW_MODEL, lazy: async () => { @@ -153,7 +166,13 @@ const router = createBrowserRouter([ }; }, }, - //Edit + /** + * Model creation routes ends + */ + + /** + * Model edit routes starts + */ { path: APPLICATION_ROUTES.EDIT_MODEL_DETAILS, lazy: async () => { @@ -222,6 +241,12 @@ const router = createBrowserRouter([ }, ], }, + /** + * Model edit routes ends + */ + /** + * Training dataset route starts + */ { path: APPLICATION_ROUTES.TRAINING_DATASETS, lazy: async () => { @@ -237,23 +262,36 @@ const router = createBrowserRouter([ }; }, }, + + /** + * Training dataset route ends + */ + + /** + * Start mapping route starts + */ { path: APPLICATION_ROUTES.START_MAPPING, lazy: async () => { const { StartMappingPage } = await import( - "@/app/routes/start-mapping/start-mapping" + "@/app/routes/start-mapping" ); return { Component: () => ( - - - + ), }; }, }, + /** + * Start mapping route ends + */ + + /** + * User account routes start + */ { path: APPLICATION_ROUTES.ACCOUNT_SETTINGS, lazy: async () => { @@ -268,14 +306,21 @@ const router = createBrowserRouter([ { path: APPLICATION_ROUTES.ACCOUNT_MODELS, lazy: async () => { - const { UserAccountModelsPage } = await import( + const { UserModelsPage } = await import( "@/app/routes/account/models" ); return { - Component: () => , + Component: () => , }; }, }, + /** + * User account routes ends + */ + + /** + * 404 route + */ { path: APPLICATION_ROUTES.NOTFOUND, lazy: async () => { @@ -283,6 +328,9 @@ const router = createBrowserRouter([ return { Component: PageNotFound }; }, }, + /** + * Catch all route -> 404 + */ { path: "*", element: ( diff --git a/frontend/src/app/routes/about.tsx b/frontend/src/app/routes/about.tsx index d1402bb8..2898ad9a 100644 --- a/frontend/src/app/routes/about.tsx +++ b/frontend/src/app/routes/about.tsx @@ -1,5 +1,43 @@ -import { PageUnderConstruction } from "@/components/errors"; +import { Header } from "@/components/shared"; +import { Image } from "@/components/ui/image"; +import HOTTeamLandscape from "@/assets/images/hot_team_landscape.png"; +import { Head } from "@/components/seo"; +import AIIcon from "@/assets/svgs/fair_ai_icon.svg"; +import { aboutPageContent } from "@/constants"; export const AboutPage = () => { - return ; + return ( +
+ +
+
+
+

+ {aboutPageContent.heroHeading.firstSegment}{" "} + + {aboutPageContent.heroHeading.secondSegment} + {" "} + {aboutPageContent.heroHeading.thirdSegment}{" "} +

+
+
+
+ {aboutPageContent.imageAlt} +
+
+
+

+ {aboutPageContent.bodyContent.firstParagraph} +

+

{aboutPageContent.bodyContent.secondParagraph}

+
+ AI Icon +
+
+ ); }; diff --git a/frontend/src/app/routes/account/models.tsx b/frontend/src/app/routes/account/models.tsx index 8b81868c..cafd222d 100644 --- a/frontend/src/app/routes/account/models.tsx +++ b/frontend/src/app/routes/account/models.tsx @@ -1,5 +1,188 @@ -import { PageUnderConstruction } from "@/components/errors"; +import Pagination, { PAGE_LIMIT } from "@/components/shared/pagination"; +import { Head } from "@/components/seo"; +import { LayoutView } from "@/enums/models"; +import { LayoutToggle, PageHeader } from "@/features/models/components"; +import { MobileModelFiltersDialog } from "@/features/models/components/dialogs"; +import { + CategoryFilter, + ClearFilters, + DateRangeFilter, + MobileFilter, + OrderingFilter, + SearchFilter, + StatusFilter, +} from "@/features/models/components/filters"; +import { useModelsListFilters } from "@/features/models/hooks/use-models"; +import { + ModelListGridLayout, + ModelListTableLayout, +} from "@/features/models/layouts"; +import { useDialog } from "@/hooks/use-dialog"; +import { APP_CONTENT } from "@/utils"; +import { useMemo } from "react"; +import ModelNotFound from "@/features/models/components/model-not-found"; +import { SEARCH_PARAMS } from "@/app/routes/models/models-list"; +import { useAuth } from "@/app/providers/auth-provider"; +import { modelPagesContent } from "@/constants"; -export const UserAccountModelsPage = () => { - return ; +export const UserModelsPage = () => { + const { isOpened, openDialog, closeDialog } = useDialog(); + const { user } = useAuth(); + + const { + clearAllFilters, + data, + isError, + isPending, + isPlaceholderData, + query, + updateQuery, + } = useModelsListFilters(undefined, user?.osm_id); + + // Since it's just a static filter, it's better to memoize it. + const memoizedCategoryFilter = useMemo( + () => , + [isPending], + ); + + const renderContent = () => { + if (data?.count === 0) { + return ; + } + + if (query[SEARCH_PARAMS.layout] === LayoutView.LIST) { + return ( +
+ +
+ ); + } + return ( + + ); + }; + + return ( + <> + + +
+ + {/* Filters */} +
+
+
+
+ + {memoizedCategoryFilter} + + {/* Mobile filters */} +
+ + +
+ + {/* Desktop */} + +
+
+ {/* Desktop */} + +
+
+ {/* Mobile */} +
+ +
+
+ {isPending ? ( +
+ ) : ( +
+
+

+ {data?.count}{" "} + { + APP_CONTENT.models.modelsList.sortingAndPaginationSection + .modelCountSuffix + } +

+
+
+ +
+ +
+
+
+ )} +
+ + {renderContent()} + + {/* mobile pagination */} +
+ +
+
+ + ); }; diff --git a/frontend/src/app/routes/landing.tsx b/frontend/src/app/routes/landing.tsx index d35cd7f8..f1dab738 100644 --- a/frontend/src/app/routes/landing.tsx +++ b/frontend/src/app/routes/landing.tsx @@ -1,10 +1,10 @@ -import { Header } from "@/components/ui/header"; +import { Header } from "@/components/landing/header"; import WhatIsFAIR from "@/components/landing/about-fair/about-fair"; import CoreFeatures from "@/components/landing/core-features/core-features"; import Corevalues from "@/components/landing/core-values/core-values"; import CallToAction from "@/components/landing/cta/cta"; -import TheFAIRProcess from "@/components/landing/fair-process/fair-process"; -import FAQs from "@/components/landing/faqs/faqs"; +import TheFAIRProcess from "@/components/shared/fair-process/fair-process"; +import { FAQs } from "@/components/shared"; import Kpi from "@/components/landing/kpi/kpi"; import TaglineBanner from "@/components/landing/tagline/tagline"; import { Head } from "@/components/seo"; diff --git a/frontend/src/app/routes/learn.tsx b/frontend/src/app/routes/learn.tsx index 7678e3c0..32e518d8 100644 --- a/frontend/src/app/routes/learn.tsx +++ b/frontend/src/app/routes/learn.tsx @@ -1,5 +1,154 @@ -import { PageUnderConstruction } from "@/components/errors"; +import { TheFAIRProcess } from "@/components/landing"; +import { Header, SectionHeader } from "@/components/shared"; +import { Image } from "@/components/ui/image"; +import fAIrValues from "@/assets/svgs/fair_values.svg"; +import { ExternalLinkIcon, YouTubePlayCircleIcon } from "@/components/ui/icons"; +import { Button } from "@/components/ui/button"; +import { Link } from "@/components/ui/link"; +import { SHOELACE_SIZES } from "@/enums"; +import { Head } from "@/components/seo"; +import { useState } from "react"; +import VideoPlaceholderImage from "@/assets/images/header_bg.jpg"; +import { learnPageContent } from "@/constants"; +import { TGuide, TVideo } from "@/types"; export const LearnPage = () => { - return ; + return ( +
+ +
+
+
+

+ {learnPageContent.heroHeading.firstSegment}{" "} + + {learnPageContent.heroHeading.secondSegment} + {" "} + {learnPageContent.heroHeading.thirdSegment}{" "} + + {learnPageContent.heroHeading.fourthSegment} + {" "} + {learnPageContent.heroHeading.fifthSegment}{" "} + + {learnPageContent.heroHeading.sixthSegment} + {" "} + {learnPageContent.heroHeading.seventhSegment} +

+

+ {learnPageContent.heroDescription} +

+
+
+ fAIr Values +
+
+ +
+ +
+ {learnPageContent.guides.map((guide, id) => ( + + ))} +
+
+
+
+ +
+ {learnPageContent.videos.map((video, id) => ( + + ))} +
+
+
+ ); +}; + +const GuideCard = ({ guide }: { guide: TGuide }) => { + return ( +
+
+
+

+ {guide.title} +

+

+ {guide.description} +

+
+
+ +
+
+
+ {guide.isLink ? ( + + + + ) : ( + + )} +
+
+ ); +}; + +const VideoCard = ({ video }: { video: TVideo }) => { + const [playVideo, setPlayVideo] = useState(false); + return ( +
+
+
+ {video.title} +
+
+ {!playVideo ? ( + + ) : ( + + )} +
+
+
+

+ {video.title} +

+

+ {video.description} +

+
+
+ ); }; diff --git a/frontend/src/app/routes/models/confirmation.tsx b/frontend/src/app/routes/models/confirmation.tsx index 3a3671e1..9d1f627f 100644 --- a/frontend/src/app/routes/models/confirmation.tsx +++ b/frontend/src/app/routes/models/confirmation.tsx @@ -14,7 +14,11 @@ export const ModelConfirmationPage = () => { const { isEditMode } = useModelsContext(); return ( -
+
{

{MODEL_CREATION_CONTENT.confirmation.description}

-
+
{ const { id } = useParams<{ id: string }>(); - const { isOpened, closeDialog, openDialog } = useDialog(); + const { isOpened: isModelFilesDialogOpened, closeDialog: closeModelFilesDialog, openDialog: openModelFilesDialog, } = useDialog(); + const navigate = useNavigate(); - const { data, isPending, isError, error } = useModelDetails(id as string, id !== undefined, 10000); - const { user } = useAuth(); + + const { data, isPending, isError, error } = useModelDetails( + id as string, + !!id, + 10000, + ); + const { isAuthenticated } = useAuth(); useEffect(() => { if (isError) { @@ -52,10 +57,23 @@ export const ModelDetailsPage = () => { closeDialog: closeModelEnhancementDialog, openDialog: openModelEnhancementDialog, } = useDialog(); - if (isPending || isError) { + + const { + isPending: isTrainingDatasetPending, + data: trainingDataset, + isError: isTrainingDatasetError, + } = useGetTrainingDataset(data?.dataset, !!data); + + const { isOpened, closeDialog, openDialog } = useDialog(); + + if ( + isPending || + isError || + isTrainingDatasetPending || + isTrainingDatasetError + ) { return ; } - const isOwner = user?.osm_id === data?.user?.osm_id; return ( <> @@ -64,6 +82,12 @@ export const ModelDetailsPage = () => { closeDialog={closeModelEnhancementDialog} modelId={data?.id as string} /> + { trainingId={data?.published_training as number} datasetId={data?.dataset as number} /> - - + +
{ size="medium" prefixIcon={StarStackIcon} onClick={openModelEnhancementDialog} + disabled={!isAuthenticated} />
{/* mobile */} @@ -123,7 +151,7 @@ export const ModelDetailsPage = () => { size="medium" prefixIcon={StarStackIcon} onClick={openModelEnhancementDialog} - disabled={!isOwner} + disabled={!isAuthenticated} />
{ modelOwner={data?.user.username as string} datasetId={data?.dataset as number} baseModel={data?.base_model as string} + tmsUrl={trainingDataset.source_imagery} />
); -}; +}; \ No newline at end of file diff --git a/frontend/src/app/routes/models/models-list.tsx b/frontend/src/app/routes/models/models-list.tsx index a899e8ed..76b7a3d2 100644 --- a/frontend/src/app/routes/models/models-list.tsx +++ b/frontend/src/app/routes/models/models-list.tsx @@ -1,40 +1,38 @@ import { - useModels, + useModelsListFilters, useModelsMapData, } from "@/features/models/hooks/use-models"; -import { useCallback, useEffect, useMemo, useState } from "react"; -import { useSearchParams } from "react-router-dom"; -import { Button } from "@/components/ui/button"; -import { CategoryIcon, FilterIcon, ListIcon } from "@/components/ui/icons"; -import { Switch } from "@/components/ui/form"; +import { useEffect, useMemo } from "react"; import { ModelListGridLayout, ModelListTableLayout, } from "@/features/models/layouts"; -import { ModelsMap } from "@/features/models/components"; +import { + LayoutToggle, + ModelMapToggle, + ModelsMap, +} from "@/features/models/components"; import { CategoryFilter, + ClearFilters, DateRangeFilter, + MobileFilter, OrderingFilter, SearchFilter, } from "@/features/models/components/filters"; -import Pagination, { PAGE_LIMIT } from "@/components/pagination"; -import { APP_CONTENT, buildDateFilterQueryString } from "@/utils"; +import Pagination, { PAGE_LIMIT } from "@/components/shared/pagination"; +import { APP_CONTENT } from "@/utils"; import { PageHeader } from "@/features/models/components/"; -import { dateFilters } from "@/features/models/components/filters/date-range-filter"; -import { ORDERING_FIELDS } from "@/features/models/components/filters/ordering-filter"; -import { FeatureCollection, TQueryParams } from "@/types"; +import { FeatureCollection } from "@/types"; import ModelNotFound from "@/features/models/components/model-not-found"; -import useDebounce from "@/hooks/use-debounce"; import { useDialog } from "@/hooks/use-dialog"; import { MobileModelFiltersDialog } from "@/features/models/components/dialogs"; import { Head } from "@/components/seo"; -import { ModelsProvider } from "@/app/providers/models-provider"; - -export enum LayoutView { - LIST = "list", - GRID = "grid", -} +import { LayoutView } from "@/enums/models"; +import { + useScrollToElement, + useScrollToTop, +} from "@/hooks/use-scroll-to-element"; export const SEARCH_PARAMS = { startDate: "start_date", @@ -46,211 +44,24 @@ export const SEARCH_PARAMS = { dateFilter: "dateFilter", layout: "layout", id: "id", -}; - -const ClearFilters = ({ - query, - clearAllFilters, - isMobile, -}: { - clearAllFilters: (event: React.ChangeEvent) => void; - query: TQueryParams; - isMobile?: boolean; -}) => { - const canClearAllFilters = Boolean( - query[SEARCH_PARAMS.searchQuery] || - query[SEARCH_PARAMS.startDate] || - query[SEARCH_PARAMS.endDate] || - query[SEARCH_PARAMS.id], - ); - - return ( -
- {canClearAllFilters ? ( - // @ts-expect-error bad type definition - - ) : null} -
- ); -}; - -const SetMapToggle = ({ - query, - updateQuery, - isMobile, -}: { - updateQuery: (params: TQueryParams) => void; - query: TQueryParams; - isMobile?: boolean; -}) => { - return ( -
-

- {APP_CONTENT.models.modelsList.filtersSection.mapViewToggleText} -

- { - updateQuery({ - [SEARCH_PARAMS.mapIsActive]: !query[SEARCH_PARAMS.mapIsActive], - }); - }} - /> -
- ); -}; - -const LayoutToggle = ({ - query, - updateQuery, - isMobile, - disabled = false, -}: { - updateQuery: (params: TQueryParams) => void; - query: TQueryParams; - isMobile?: boolean; - disabled?: boolean; -}) => { - const activeLayout = query[SEARCH_PARAMS.layout]; - return ( - - ); -}; - -const MobileFilter = ({ - openMobileFilterModal, -}: { - openMobileFilterModal: () => void; - isMobile?: boolean; -}) => { - return ( -
- {} -
- ); + status: "status", }; export const ModelsPage = () => { - const [searchParams, setSearchParams] = useSearchParams(); - - const defaultQueries = { - [SEARCH_PARAMS.offset]: 0, - [SEARCH_PARAMS.searchQuery]: - searchParams.get(SEARCH_PARAMS.searchQuery) || "", - [SEARCH_PARAMS.ordering]: - searchParams.get(SEARCH_PARAMS.ordering) || - (ORDERING_FIELDS[1].apiValue as string), - [SEARCH_PARAMS.mapIsActive]: - searchParams.get(SEARCH_PARAMS.mapIsActive) || false, - [SEARCH_PARAMS.startDate]: searchParams.get(SEARCH_PARAMS.startDate) || "", - [SEARCH_PARAMS.endDate]: searchParams.get(SEARCH_PARAMS.endDate) || "", - [SEARCH_PARAMS.dateFilter]: - searchParams.get(SEARCH_PARAMS.dateFilter) || dateFilters[0].searchParams, - [SEARCH_PARAMS.layout]: - searchParams.get(SEARCH_PARAMS.layout) || LayoutView.GRID, - [SEARCH_PARAMS.id]: searchParams.get(SEARCH_PARAMS.id) || "", - }; - - const [query, setQuery] = useState(defaultQueries); - const { isOpened, openDialog, closeDialog } = useDialog(); - - const debouncedSearchText = useDebounce( - query[SEARCH_PARAMS.searchQuery] as string, - 300, - ); - - const { data, isPending, isPlaceholderData, isError } = useModels({ - searchQuery: debouncedSearchText, - limit: PAGE_LIMIT, - offset: query[SEARCH_PARAMS.offset] as number, - orderBy: query[SEARCH_PARAMS.ordering] as string, - id: query[SEARCH_PARAMS.id] as number, - dateFilters: buildDateFilterQueryString( - dateFilters.find( - (filter) => filter.searchParams === query[SEARCH_PARAMS.dateFilter], - ), - query[SEARCH_PARAMS.startDate] as string, - query[SEARCH_PARAMS.endDate] as string, - ), - }); - - const updateQuery = useCallback( - (newParams: TQueryParams) => { - setQuery((prevQuery) => ({ - ...prevQuery, - ...newParams, - })); - const updatedParams = new URLSearchParams(searchParams); - - Object.entries(newParams).forEach(([key, value]) => { - if (value) { - updatedParams.set(key, String(value)); - } else { - updatedParams.delete(key); - } - }); - - setSearchParams(updatedParams); - }, - [searchParams, setSearchParams], - ); - - //reset offset back to 0 when searching or when ID filtering is applied from the map. - useEffect(() => { - if ( - (query[SEARCH_PARAMS.searchQuery] !== "" || - query[SEARCH_PARAMS.id] !== "") && - (query[SEARCH_PARAMS.offset] as number) > 0 - ) { - updateQuery({ [SEARCH_PARAMS.offset]: 0 }); - } - }, [query]); - - useEffect(() => { - const newQuery = { - [SEARCH_PARAMS.offset]: defaultQueries[SEARCH_PARAMS.offset], - [SEARCH_PARAMS.ordering]: defaultQueries[SEARCH_PARAMS.ordering], - [SEARCH_PARAMS.mapIsActive]: defaultQueries[SEARCH_PARAMS.mapIsActive], - [SEARCH_PARAMS.startDate]: defaultQueries[SEARCH_PARAMS.startDate], - [SEARCH_PARAMS.endDate]: defaultQueries[SEARCH_PARAMS.endDate], - [SEARCH_PARAMS.dateFilter]: defaultQueries[SEARCH_PARAMS.dateFilter], - [SEARCH_PARAMS.layout]: defaultQueries[SEARCH_PARAMS.layout], - [SEARCH_PARAMS.searchQuery]: defaultQueries[SEARCH_PARAMS.searchQuery], - [SEARCH_PARAMS.id]: defaultQueries[SEARCH_PARAMS.id], - }; - setQuery(newQuery); - }, []); + const mapViewElementId = "map-view"; + const { scrollToElement } = useScrollToElement(mapViewElementId); + const { scrollToTop } = useScrollToTop(); + const { + clearAllFilters, + data, + isError, + isPending, + isPlaceholderData, + query, + updateQuery, + mapViewIsActive, + } = useModelsListFilters(0); const { data: mapData, @@ -264,47 +75,34 @@ export const ModelsPage = () => { [isPending], ); - const mapViewIsActive = useMemo( - () => query[SEARCH_PARAMS.mapIsActive], - [query], - ); - - const clearAllFilters = useCallback(() => { - const resetParams = new URLSearchParams(); - setSearchParams(resetParams); - setQuery((prev) => ({ - // Preserve existing query params - ...prev, - // Clear only the filter fields - [SEARCH_PARAMS.searchQuery]: "", - [SEARCH_PARAMS.startDate]: "", - [SEARCH_PARAMS.endDate]: "", - [SEARCH_PARAMS.id]: "", - })); - }, []); + // Mapview toggling interaction + useEffect(() => { + if (mapViewIsActive) { + scrollToElement(); + } else { + scrollToTop(); + } + }, [mapViewIsActive]); const renderContent = () => { if (data?.count === 0) { - return ( -
- -
- ); + return ; } if (mapViewIsActive) { return ( -
-
-
- -
+
+
+
-
+
{modelsMapDataIsPending || modelsMapDataIsError ? (
) : ( @@ -340,7 +138,7 @@ export const ModelsPage = () => { return ( <> - + { disabled={isPending} />
- {/* - Providing access to the models context, so that the 'create model' button can reset the store before going to the model creation form. - */} - - - -
-
-
- - {memoizedCategoryFilter} - {/* Mobile filters */} -
- + + {/* Filters */} +
+
+
+
+ + {memoizedCategoryFilter} + {/* Mobile filters */} +
+ + +
+ + {/* Desktop */} + +
+
+ {/* Desktop */} +
- - {/* Desktop */} -
-
- {/* Desktop */} - - +
- {/* Mobile */} -
- -
-
- - {isPending ? ( -
- ) : ( -
-
-

- {data?.count}{" "} - { - APP_CONTENT.models.modelsList.sortingAndPaginationSection - .modelCountSuffix - } -

- -
-
- -
-
+ ) : ( +
+
+

+ {data?.count}{" "} + { + APP_CONTENT.models.modelsList.sortingAndPaginationSection + .modelCountSuffix + } +

+ +
+
+ +
+ +
-
- )} - -
- {renderContent()} + )}
+ {renderContent()} {/* mobile pagination */} -
+
{ - return ; + return ( +
+ +
+
+
+

+ {resourcesPageContent.hero.firstSegment}{" "} + + {resourcesPageContent.hero.secondSegment} + {" "} + {resourcesPageContent.hero.thirdSegment}{" "} +

+
+
+
+ +
+
+ +
+ {resourcesPageContent.articles.articles.map((article, id) => ( + + ))} +
+
+
+ ); +}; + +const ArticleCard = ({ article }: { article: TArticle }) => { + return ( +
+
+
+ {article.title} +
+
+
+

+ {truncateString(article.title, 50)} +

+

+ {truncateString(article.snippet, 120)} +

+ +

+ Read more +

+ +
+
+ ); }; diff --git a/frontend/src/app/routes/start-mapping.tsx b/frontend/src/app/routes/start-mapping.tsx new file mode 100644 index 00000000..a0b11e31 --- /dev/null +++ b/frontend/src/app/routes/start-mapping.tsx @@ -0,0 +1,168 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useNavigate, useParams, useSearchParams } from "react-router-dom"; +import { Head } from "@/components/seo"; +import { BBOX, TileJSON, TModelPredictions } from "@/types"; +import { useModelDetails } from "@/features/models/hooks/use-models"; +import { useGetTrainingDataset } from "@/features/models/hooks/use-dataset"; +import { + StartMappingHeader, + StartMappingMapComponent, +} from "@/features/start-mapping/components"; +import { useGetTMSTileJSON } from "@/features/model-creation/hooks/use-tms-tilejson"; +import { + APPLICATION_ROUTES, + extractTileJSONURL, + PREDICTION_API_FILE_EXTENSIONS, +} from "@/utils"; +import { BASE_MODELS } from "@/enums"; +import { startMappingPageContent } from "@/constants"; +import { useMapInstance } from "@/hooks/use-map-instance"; + +export const SEARCH_PARAMS = { + useJOSMQ: "useJOSMQ", + confidenceLevel: "confidenceLevel", + tolerance: "tolerance", + area: "area", +}; + +export type TQueryParams = { [x: string]: string | number | boolean }; + +export const StartMappingPage = () => { + const { modelId } = useParams(); + const [searchParams, setSearchParams] = useSearchParams(); + const { map, mapContainerRef, currentZoom } = useMapInstance(); + const defaultQueries = { + [SEARCH_PARAMS.useJOSMQ]: searchParams.get(SEARCH_PARAMS.useJOSMQ) || false, + [SEARCH_PARAMS.confidenceLevel]: + searchParams.get(SEARCH_PARAMS.confidenceLevel) || 90, + [SEARCH_PARAMS.tolerance]: searchParams.get(SEARCH_PARAMS.tolerance) || 0.3, + [SEARCH_PARAMS.area]: searchParams.get(SEARCH_PARAMS.area) || 4, + }; + const [query, setQuery] = useState(defaultQueries); + + const { isError, isPending, data, error } = useModelDetails( + modelId as string, + !!modelId, + ); + + const { + data: trainingDataset, + isPending: trainingDatasetIsPending, + isError: trainingDatasetIsError, + } = useGetTrainingDataset(data?.dataset as number, !isPending); + + const tileJSONURL = extractTileJSONURL(trainingDataset?.source_imagery ?? ""); + + const { + data: oamTileJSON, + isError: oamTileJSONIsError, + error: oamTileJSONError, + } = useGetTMSTileJSON(tileJSONURL); + + const navigate = useNavigate(); + + useEffect(() => { + if (isError) { + navigate(APPLICATION_ROUTES.NOTFOUND, { + state: { + from: window.location.pathname, + //@ts-expect-error bad type definition + error: error?.response?.data?.detail, + }, + }); + } + }, [isError, error, navigate]); + + const [modelPredictions, setModelPredictions] = useState({ + all: [], + accepted: [], + rejected: [], + }); + + const modelPredictionsExist = useMemo( + () => + modelPredictions.accepted.length > 0 || + modelPredictions.rejected.length > 0 || + modelPredictions.all.length > 0, + [modelPredictions], + ); + + const updateQuery = useCallback( + (newParams: TQueryParams) => { + setQuery((prevQuery) => ({ + ...prevQuery, + ...newParams, + })); + const updatedParams = new URLSearchParams(searchParams); + Object.entries(newParams).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + updatedParams.set(key, String(value)); + } else { + updatedParams.delete(key); + } + }); + setSearchParams(updatedParams, { replace: true }); + }, + [searchParams, setSearchParams], + ); + const bounds = map?.getBounds(); + + const trainingConfig = { + tolerance: query[SEARCH_PARAMS.tolerance] as number, + area_threshold: query[SEARCH_PARAMS.area] as number, + use_josm_q: query[SEARCH_PARAMS.useJOSMQ] as boolean, + confidence: query[SEARCH_PARAMS.confidenceLevel] as number, + checkpoint: `/mnt/efsmount/data/trainings/dataset_${data?.dataset}/output/training_${data?.published_training}/checkpoint${PREDICTION_API_FILE_EXTENSIONS[data?.base_model as BASE_MODELS]}`, + max_angle_change: 15, + model_id: modelId as string, + skew_tolerance: 15, + source: trainingDataset?.source_imagery as string, + zoom_level: currentZoom, + bbox: [ + bounds?.getWest(), + bounds?.getSouth(), + bounds?.getEast(), + bounds?.getNorth(), + ] as BBOX, + }; + + return ( + <> + +
+
+ +
+
+ +
+
+ + ); +}; diff --git a/frontend/src/app/routes/start-mapping/start-mapping.tsx b/frontend/src/app/routes/start-mapping/start-mapping.tsx deleted file mode 100644 index dc65903e..00000000 --- a/frontend/src/app/routes/start-mapping/start-mapping.tsx +++ /dev/null @@ -1,292 +0,0 @@ -import { useMap } from "@/app/providers/map-provider"; -import { MapComponent, MapCursorToolTip } from "@/components/map"; -import { BackButton, Button, ButtonWithIcon } from "@/components/ui/button"; -import { Divider } from "@/components/ui/divider"; -import { DropDown } from "@/components/ui/dropdown"; -import { FormLabel, Input, Select, Switch } from "@/components/ui/form"; -import { ChevronDownIcon, TagsInfoIcon } from "@/components/ui/icons"; -import { SkeletonWrapper } from "@/components/ui/skeleton"; -import { INPUT_TYPES, SHOELACE_SIZES } from "@/enums"; -import { ModelDetailsPopUp } from "@/features/models/components"; -import { useModelDetails } from "@/features/models/hooks/use-models"; -import { useDropdownMenu } from "@/hooks/use-dropdown-menu"; -import { useToolTipVisibility } from "@/hooks/use-tooltip-visibility"; -import { APPLICATION_ROUTES, truncateString } from "@/utils"; -import { useCallback, useEffect, useState } from "react"; -import { useNavigate, useParams, useSearchParams } from "react-router-dom"; - -const zoomInMessage = "Zoom in to at least zoom 19 to start mapping."; - -const SEARCH_PARAMS = { - useJOSMQ: "useJOSMQ", - confidenceLevel: "confidenceLevel", - tolerance: "tolerance", - area: "area", -}; - -const defaultQueries = { - [SEARCH_PARAMS.useJOSMQ]: false, - [SEARCH_PARAMS.confidenceLevel]: 90, - [SEARCH_PARAMS.tolerance]: 0.3, - [SEARCH_PARAMS.area]: 4, -}; - -type TQueryParams = typeof defaultQueries; - -const confidenceLevels = [ - { - name: "25%", - value: 25, - }, - { - name: "50%", - value: 50, - }, - { - name: "75%", - value: 75, - }, - { - name: "90%", - value: 90, - }, -]; - -export const StartMappingPage = () => { - const { modelId } = useParams(); - const { isError, isPending, data, error } = useModelDetails( - modelId as string, - modelId !== undefined, - ); - const navigate = useNavigate(); - const { currentZoom } = useMap(); - - const [searchParams, setSearchParams] = useSearchParams(); - const [query, setQuery] = useState(defaultQueries); - const { tooltipPosition, tooltipVisible } = useToolTipVisibility(); - const { onDropdownHide, onDropdownShow, dropdownIsOpened } = - useDropdownMenu(); - const [showModelDetails, setShowModelDetails] = useState(false); - - useEffect(() => { - if (isError) { - navigate(APPLICATION_ROUTES.NOTFOUND, { - state: { - from: window.location.pathname, - //@ts-expect-error bad type definition - error: error?.response?.data?.detail, - }, - }); - } - }, [isError, error, navigate]); - - const updateQuery = useCallback( - (newParams: TQueryParams) => { - setQuery((prevQuery) => ({ - ...prevQuery, - ...newParams, - })); - const updatedParams = new URLSearchParams(searchParams); - Object.entries(newParams).forEach(([key, value]) => { - if (value) { - updatedParams.set(key, String(value)); - } else { - updatedParams.delete(key); - } - }); - setSearchParams(updatedParams); - }, - [searchParams, setSearchParams], - ); - - useEffect(() => { - const newQuery = { - [SEARCH_PARAMS.useJOSMQ]: Boolean( - searchParams.get(SEARCH_PARAMS.useJOSMQ), - ), - [SEARCH_PARAMS.confidenceLevel]: Number( - searchParams.get(SEARCH_PARAMS.confidenceLevel), - ), - [SEARCH_PARAMS.tolerance]: Number( - searchParams.get(SEARCH_PARAMS.tolerance), - ), - [SEARCH_PARAMS.area]: Number(searchParams.get(SEARCH_PARAMS.area)), - }; - setQuery(newQuery); - }, []); - - const disableButtons = currentZoom < 19; - - const downloadButtonDropdownOptions = [ - { - name: "All Features as GeoJSON", - value: "All Features as GeoJSON", - }, - { - name: "Mapped Features Only", - value: "Mapped Features Only", - }, - { - name: "Open in JSOM", - value: "Open in JOSM", - }, - ]; - - const popupAnchor = "model-details"; - - return ( - - -
-
-
-
-

- {data?.name - ? truncateString(data?.name, 40) - : "Localidad Ama Chuma (Patacamaya)"} -

- setShowModelDetails(false)} - anchor={popupAnchor} - model={data} - /> - -
-
-

- Map Data - Accepted: 0 Rejected: 0{" "} -

- - } - /> -
-
- -
-
-

Use JOSM Q

- { - updateQuery({ - [SEARCH_PARAMS.useJOSMQ]: event.target.checked, - }); - }} - /> -
- - - updateQuery({ - [SEARCH_PARAMS.tolerance]: Number(event.target.value), - }) - } - /> -
-
- - - updateQuery({ - [SEARCH_PARAMS.area]: Number(event.target.value), - }) - } - /> -
-
-
- {disableButtons && ( -

- {zoomInMessage} -

- )} - -
-
-
-
- - -

{zoomInMessage}

-
-
-
-
-
- ); -}; diff --git a/frontend/src/assets/images/cc_by_badge.png b/frontend/src/assets/images/cc_by_badge.png index c2e4a80f..0ca88521 100644 Binary files a/frontend/src/assets/images/cc_by_badge.png and b/frontend/src/assets/images/cc_by_badge.png differ diff --git a/frontend/src/assets/images/fair_model_placeholder_image.png b/frontend/src/assets/images/fair_model_placeholder_image.png deleted file mode 100644 index c5ead129..00000000 Binary files a/frontend/src/assets/images/fair_model_placeholder_image.png and /dev/null differ diff --git a/frontend/src/assets/images/header_bg.jpg b/frontend/src/assets/images/header_bg.jpg new file mode 100644 index 00000000..13ce0144 Binary files /dev/null and b/frontend/src/assets/images/header_bg.jpg differ diff --git a/frontend/src/assets/images/header_bg.png b/frontend/src/assets/images/header_bg.png deleted file mode 100644 index 93525781..00000000 Binary files a/frontend/src/assets/images/header_bg.png and /dev/null differ diff --git a/frontend/src/assets/images/hot_team.jpg b/frontend/src/assets/images/hot_team.jpg index a22a0334..8ddaec13 100644 Binary files a/frontend/src/assets/images/hot_team.jpg and b/frontend/src/assets/images/hot_team.jpg differ diff --git a/frontend/src/assets/images/hot_team_2.jpg b/frontend/src/assets/images/hot_team_2.jpg index 07454520..1846a59c 100644 Binary files a/frontend/src/assets/images/hot_team_2.jpg and b/frontend/src/assets/images/hot_team_2.jpg differ diff --git a/frontend/src/assets/images/hot_team_landscape.png b/frontend/src/assets/images/hot_team_landscape.png new file mode 100644 index 00000000..79760de3 Binary files /dev/null and b/frontend/src/assets/images/hot_team_landscape.png differ diff --git a/frontend/src/assets/images/mapathon_ongoing.jpg b/frontend/src/assets/images/mapathon_ongoing.jpg index 766dde8e..f50cb005 100644 Binary files a/frontend/src/assets/images/mapathon_ongoing.jpg and b/frontend/src/assets/images/mapathon_ongoing.jpg differ diff --git a/frontend/src/assets/images/model_creation_success.png b/frontend/src/assets/images/model_creation_success.png index 346c755a..5cd0af29 100644 Binary files a/frontend/src/assets/images/model_creation_success.png and b/frontend/src/assets/images/model_creation_success.png differ diff --git a/frontend/src/assets/images/model_placeholder_image.png b/frontend/src/assets/images/model_placeholder_image.png index 5cdea070..b6a0e23a 100644 Binary files a/frontend/src/assets/images/model_placeholder_image.png and b/frontend/src/assets/images/model_placeholder_image.png differ diff --git a/frontend/src/assets/images/training_in_progress.png b/frontend/src/assets/images/training_in_progress.png index 9f1cb16b..d293df40 100644 Binary files a/frontend/src/assets/images/training_in_progress.png and b/frontend/src/assets/images/training_in_progress.png differ diff --git a/frontend/src/assets/svgs/fair_values.svg b/frontend/src/assets/svgs/fair_values.svg new file mode 100644 index 00000000..8c82424b --- /dev/null +++ b/frontend/src/assets/svgs/fair_values.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/src/assets/svgs/header_bg_contour.svg b/frontend/src/assets/svgs/header_bg_contour.svg new file mode 100644 index 00000000..4b47840a --- /dev/null +++ b/frontend/src/assets/svgs/header_bg_contour.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/assets/socials/facebook_logo.svg b/frontend/src/assets/svgs/socials/facebook_logo.svg similarity index 100% rename from frontend/src/assets/socials/facebook_logo.svg rename to frontend/src/assets/svgs/socials/facebook_logo.svg diff --git a/frontend/src/assets/socials/github_logo.svg b/frontend/src/assets/svgs/socials/github_logo.svg similarity index 100% rename from frontend/src/assets/socials/github_logo.svg rename to frontend/src/assets/svgs/socials/github_logo.svg diff --git a/frontend/src/assets/socials/instagram_logo.svg b/frontend/src/assets/svgs/socials/instagram_logo.svg similarity index 100% rename from frontend/src/assets/socials/instagram_logo.svg rename to frontend/src/assets/svgs/socials/instagram_logo.svg diff --git a/frontend/src/assets/socials/x_logo.svg b/frontend/src/assets/svgs/socials/x_logo.svg similarity index 100% rename from frontend/src/assets/socials/x_logo.svg rename to frontend/src/assets/svgs/socials/x_logo.svg diff --git a/frontend/src/assets/socials/youtube_logo.svg b/frontend/src/assets/svgs/socials/youtube_logo.svg similarity index 100% rename from frontend/src/assets/socials/youtube_logo.svg rename to frontend/src/assets/svgs/socials/youtube_logo.svg diff --git a/frontend/src/components/errors/fallback.tsx b/frontend/src/components/errors/fallback.tsx index 97a97534..d2a767ac 100644 --- a/frontend/src/components/errors/fallback.tsx +++ b/frontend/src/components/errors/fallback.tsx @@ -1,11 +1,9 @@ import { Button } from "@/components/ui/button"; import { APP_CONTENT } from "@/utils"; -import { NavBar } from "@/components/ui/navbar"; const MainErrorFallback = () => { return ( <> -

diff --git a/frontend/src/components/landing/about-fair/about-fair.module.css b/frontend/src/components/landing/about-fair/about-fair.module.css index e48fe87c..4613ac9b 100644 --- a/frontend/src/components/landing/about-fair/about-fair.module.css +++ b/frontend/src/components/landing/about-fair/about-fair.module.css @@ -76,7 +76,7 @@ /* lg: */ @media (min-width: 1024px) { .aboutfAIrContainer { - padding: 100px var(--hot-fair-spacing-5x-large); + padding: 100px var(--hot-fair-spacing-extra-large); flex-direction: row; justify-content: space-between; } @@ -89,4 +89,4 @@ min-width: 366px; min-height: 366px; } -} +} \ No newline at end of file diff --git a/frontend/src/components/landing/core-features/core-features.module.css b/frontend/src/components/landing/core-features/core-features.module.css index 1888717b..d6c90e7c 100644 --- a/frontend/src/components/landing/core-features/core-features.module.css +++ b/frontend/src/components/landing/core-features/core-features.module.css @@ -5,8 +5,8 @@ flex-direction: column; align-items: center; justify-content: space-around; - gap: var(--hot-fair-spacing-5x-large); - padding: var(--hot-fair-spacing-5x-large) 0; + gap: var(--hot-fair-spacing-extra-large); + padding: var(--hot-fair-spacing-extra-large) 0; } .coreFeatureItem { @@ -52,6 +52,5 @@ .coreFeatures { flex-direction: row; min-height: 200px; - padding: 0 var(--hot-fair-spacing-5x-large); } } diff --git a/frontend/src/components/landing/core-values/core-values.module.css b/frontend/src/components/landing/core-values/core-values.module.css index fc456c08..a3ffc6e3 100644 --- a/frontend/src/components/landing/core-values/core-values.module.css +++ b/frontend/src/components/landing/core-values/core-values.module.css @@ -139,7 +139,7 @@ /* md: */ @media (min-width: 768px) { .coreValues { - padding: 0 var(--hot-fair-spacing-5x-large); + padding: 0 var(--hot-fair-spacing-extra-large); margin-bottom: 200px; /* Adding the height of the rectangle */ } @@ -240,4 +240,4 @@ .dashedLineWrapper { top: 320px; } -} +} \ No newline at end of file diff --git a/frontend/src/components/landing/cta/cta.module.css b/frontend/src/components/landing/cta/cta.module.css index e32e7501..bdcd0e40 100644 --- a/frontend/src/components/landing/cta/cta.module.css +++ b/frontend/src/components/landing/cta/cta.module.css @@ -79,12 +79,12 @@ /* md: */ @media (min-width: 768px) { .container { - padding: 0 var(--hot-fair-spacing-5x-large); + padding: 0 var(--hot-fair-spacing-extra-large); min-height: 452px; align-items: center; justify-content: space-between; flex-direction: row; - gap: var(--hot-fair-spacing-5x-large); + gap: var(--hot-fair-spacing-extra-large); } .ctaContent { @@ -159,4 +159,4 @@ right: 0; left: -54px; } -} +} \ No newline at end of file diff --git a/frontend/src/components/landing/cta/cta.tsx b/frontend/src/components/landing/cta/cta.tsx index 50117ca0..67d5fd40 100644 --- a/frontend/src/components/landing/cta/cta.tsx +++ b/frontend/src/components/landing/cta/cta.tsx @@ -7,7 +7,7 @@ import { Link } from "@/components/ui/link"; const CallToAction = () => { return ( -

+

{APP_CONTENT.homepage.callToAction.title}

diff --git a/frontend/src/components/landing/faqs/faqs.tsx b/frontend/src/components/landing/faqs/faqs.tsx deleted file mode 100644 index 787e3894..00000000 --- a/frontend/src/components/landing/faqs/faqs.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import styles from "./faqs.module.css"; -import { APP_CONTENT } from "@/utils/content"; -import { Accordion } from "@/components/ui/accordion"; - -const FAQs = () => { - return ( -
-

- {APP_CONTENT.homepage.faqs.sectionTitle} -

-
-
- {APP_CONTENT.homepage.faqs.content.map((faq, id) => ( - - ))} -
-

- {APP_CONTENT.homepage.faqs.cta} - > -

-
-
- ); -}; - -export default FAQs; diff --git a/frontend/src/components/ui/header/header.module.css b/frontend/src/components/landing/header/header.module.css similarity index 50% rename from frontend/src/components/ui/header/header.module.css rename to frontend/src/components/landing/header/header.module.css index 48bd1ad2..7d0ee569 100644 --- a/frontend/src/components/ui/header/header.module.css +++ b/frontend/src/components/landing/header/header.module.css @@ -2,99 +2,6 @@ color: var(--hot-fair-color-dark); } -/* NavBar styles start */ -nav { - display: flex; - justify-content: space-between; - align-items: center; - padding: 0 var(--sl-spacing-large); -} - -.navLinkItem:hover, -.activeLink { - border-bottom: 2px solid var(--hot-fair-color-primary); -} - -.webNavLinks { - display: none; -} - -.mobileNavLinks { - display: flex; - flex-direction: column; - align-items: center; - text-transform: uppercase; - font-weight: var(--sl-spacing-medium); - font-size: var(--hot-fair-font-size-body-text-3); - gap: var(--sl-spacing-3x-large); -} - -.loginButton { - display: none; -} - -.hamburgerMenu { - display: block; -} - -/* Mobile Nav */ - -.drawerContentContainer { - display: grid; - grid-template-columns: repeat(8, 1fr); - row-gap: var(--sl-spacing-3x-large); -} - -.drawerHeaderContainer { - grid-column: span 12; - display: flex; - align-items: center; - justify-content: space-between; -} - -.navLinksContainer { - grid-column: span 12; - display: flex; - flex-direction: column; - align-items: center; - gap: var(--sl-spacing-3x-large); -} - -.loginButtonContainer { - grid-column: 3 / 7; - display: flex; - align-items: center; - justify-content: center; -} - -.closeButton { - font-size: var(--hot-fair-font-size-title-3); -} - -/* NavBar styles ends */ - -/* Profile styles starts */ - -.profileContainer { - display: none; -} - -.userProfile { - display: flex; - align-items: center; - gap: 12px; - justify-content: center; - cursor: pointer; -} - -.userProfileName { - font-size: var(--hot-fair-font-size-body-text-2); - color: var(--hot-fair-color-dark); - text-wrap: nowrap; -} - -/* Profile styles ends */ - /* Jumbotron styles begins */ .jumbotronContainer { @@ -104,7 +11,7 @@ nav { } .jumbotronContentContainer { - padding: var(--hot-fair-spacing-5x-large) var(--sl-spacing-large); + padding: var(--hot-fair-spacing-extra-large) var(--sl-spacing-large); background-color: var(--hot-fair-color-dark); display: flex; flex-direction: column; @@ -165,8 +72,8 @@ nav { flex-direction: row; justify-content: flex-start; align-items: center; - padding: var(--sl-spacing-large); - background-image: url("../../../assets/images/header_bg.png"); + padding: var(--sl-spacing-4x-large); + background-image: url("../../../assets/images/header_bg.jpg"); background-size: cover; background-position: center; background-repeat: no-repeat; @@ -186,63 +93,11 @@ nav { .jumbotronImage { display: none; } - - .userProfileName { - text-wrap: wrap; - } - - .navLinkItem:hover, - .activeLink { - border-bottom: 2px solid var(--hot-fair-color-primary); - } } /* mdx: */ @media (min-width: 960px) { - /* NavBar styles start */ - nav { - display: flex; - width: 100%; - justify-content: space-between; - align-items: center; - height: 80px; - padding: 0 var(--hot-fair-spacing-5x-large); - gap: var(--sl-spacing-medium); - } - - nav ul { - list-style: none; - margin: 0; - padding: 0; - display: flex; - align-items: center; - } - - .webNavLinks { - display: flex; - gap: var(--sl-spacing-3x-large); - } - - .loginButton { - display: flex; - } - - .hamburgerMenu { - display: none; - } - - .profileContainer { - display: block; - } - - /* NavBar styles ends */ - /* Jumbotron style begins */ - - .jumbotronContainer { - padding: var(--hot-fair-spacing-5x-large); - } - .jumbotronContentContainer { max-width: 60%; max-height: 100%; diff --git a/frontend/src/components/ui/header/header.tsx b/frontend/src/components/landing/header/header.tsx similarity index 84% rename from frontend/src/components/ui/header/header.tsx rename to frontend/src/components/landing/header/header.tsx index 4feb0449..47685837 100644 --- a/frontend/src/components/ui/header/header.tsx +++ b/frontend/src/components/landing/header/header.tsx @@ -1,6 +1,6 @@ import { Button } from "@/components/ui/button"; -import styles from "@/components/ui/header/header.module.css"; -import BackgroundImage from "@/assets/images/header_bg.png"; +import styles from "@/components/landing/header/header.module.css"; +import BackgroundImage from "@/assets/images/header_bg.jpg"; import { APP_CONTENT } from "@/utils/content"; import { Image } from "@/components/ui/image"; import { Link } from "@/components/ui/link"; @@ -9,8 +9,8 @@ import { APPLICATION_ROUTES } from "@/utils"; const Header = () => { return (
-
-
+
+

{APP_CONTENT.homepage.jumbotronTitle}

{APP_CONTENT.homepage.jumbotronHeadline}

diff --git a/frontend/src/components/ui/header/index.ts b/frontend/src/components/landing/header/index.ts similarity index 100% rename from frontend/src/components/ui/header/index.ts rename to frontend/src/components/landing/header/index.ts diff --git a/frontend/src/components/landing/index.ts b/frontend/src/components/landing/index.ts index 085a2a6d..c2c1f6e5 100644 --- a/frontend/src/components/landing/index.ts +++ b/frontend/src/components/landing/index.ts @@ -1,7 +1,6 @@ export { default as Kpi } from "./kpi/kpi"; export { default as WhatIsFAIR } from "./about-fair/about-fair"; -export { default as TheFAIRProcess } from "./fair-process/fair-process"; -export { default as FAQs } from "./faqs/faqs"; +export { default as TheFAIRProcess } from "../shared/fair-process/fair-process"; export { default as CoreFeatures } from "./core-features/core-features"; export { default as Corevalues } from "./core-values/core-values"; export { default as TaglineBanner } from "./tagline/tagline"; diff --git a/frontend/src/components/landing/kpi/kpi.module.css b/frontend/src/components/landing/kpi/kpi.module.css index a588cda8..625d6629 100644 --- a/frontend/src/components/landing/kpi/kpi.module.css +++ b/frontend/src/components/landing/kpi/kpi.module.css @@ -26,6 +26,7 @@ color: var(--hot-fair-color-dark); font-size: var(--hot-fair-font-size-body-text-2base); font-weight: var(--hot-fair-font-weight-semibold); + text-align: center; } /* md: */ diff --git a/frontend/src/components/landing/kpi/kpi.tsx b/frontend/src/components/landing/kpi/kpi.tsx index c7656628..617a44da 100644 --- a/frontend/src/components/landing/kpi/kpi.tsx +++ b/frontend/src/components/landing/kpi/kpi.tsx @@ -1,4 +1,4 @@ -import { APP_CONTENT } from "@/utils"; +import { APP_CONTENT, KPI_STATS_CACHE_TIME_MS } from "@/utils"; import styles from "./kpi.module.css"; import { API_ENDPOINTS, apiClient } from "@/services"; import { useQuery } from "@tanstack/react-query"; @@ -24,6 +24,7 @@ const Kpi = () => { const { data, isLoading, isError, error } = useQuery({ queryKey: ["kpis"], queryFn: fetchKPIStats, + refetchInterval: KPI_STATS_CACHE_TIME_MS, }); if (isError) { diff --git a/frontend/src/components/landing/tagline/tagline.module.css b/frontend/src/components/landing/tagline/tagline.module.css index c1efa071..9bcb64d5 100644 --- a/frontend/src/components/landing/tagline/tagline.module.css +++ b/frontend/src/components/landing/tagline/tagline.module.css @@ -35,6 +35,6 @@ } .taglineBanner { - padding: 0 var(--hot-fair-spacing-5x-large); + padding: 0 var(--hot-fair-spacing-extra-large); } -} +} \ No newline at end of file diff --git a/frontend/src/components/layouts/model-forms-layout.tsx b/frontend/src/components/layouts/model-forms-layout.tsx index 59b11175..8a2b0f2b 100644 --- a/frontend/src/components/layouts/model-forms-layout.tsx +++ b/frontend/src/components/layouts/model-forms-layout.tsx @@ -18,7 +18,7 @@ import { ModelsProvider, useModelsContext, } from "@/app/providers/models-provider"; -import ModelsLayout from "./models-layout"; +import { BackButton } from "../ui/button"; const pages: { id: number; @@ -76,32 +76,31 @@ const ModelCreationLayout = () => { }, [pathname]); return ( - - - - -
-
- -
- - {!pathname.includes(MODELS_ROUTES.CONFIRMATION) && ( - - )} + + + + +
+
+
- - + + {!pathname.includes(MODELS_ROUTES.CONFIRMATION) && ( + + )} +
+
); }; diff --git a/frontend/src/components/layouts/models-layout.tsx b/frontend/src/components/layouts/models-layout.tsx deleted file mode 100644 index 401a6a14..00000000 --- a/frontend/src/components/layouts/models-layout.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { MapProvider } from "@/app/providers/map-provider"; -import { Outlet } from "react-router-dom"; - -const ModelsLayout = ({ children }: { children?: React.ReactNode }) => { - return {children ? children : }; -}; - -export default ModelsLayout; diff --git a/frontend/src/components/layouts/root-layout.tsx b/frontend/src/components/layouts/root-layout.tsx index 8af9516a..397cd2e9 100644 --- a/frontend/src/components/layouts/root-layout.tsx +++ b/frontend/src/components/layouts/root-layout.tsx @@ -4,20 +4,24 @@ import { Footer } from "@/components/ui/footer"; import { useEffect } from "react"; import { Banner } from "@/components/ui/banner"; import { APPLICATION_ROUTES } from "@/utils"; +import { useScrollToTop } from "@/hooks/use-scroll-to-element"; const RootLayout = () => { const { pathname } = useLocation(); - // Scroll to top on page switch. + const { scrollToTop } = useScrollToTop(); + // Scroll to top on pages switch. useEffect(() => { - window.scrollTo(0, 0); + scrollToTop(); }, [pathname]); return ( -
+
- + {!pathname.includes(APPLICATION_ROUTES.START_MAPPING_BASE) && } +
diff --git a/frontend/src/components/map/controls/current-zoom-control.tsx b/frontend/src/components/map/controls/current-zoom-control.tsx new file mode 100644 index 00000000..abbe3b13 --- /dev/null +++ b/frontend/src/components/map/controls/current-zoom-control.tsx @@ -0,0 +1,7 @@ +export const ZoomLevel = ({ currentZoom }: { currentZoom: number }) => { + return ( +
+

Zoom level: {currentZoom}

+
+ ); +}; diff --git a/frontend/src/components/map/draw-control.tsx b/frontend/src/components/map/controls/draw-control.tsx similarity index 86% rename from frontend/src/components/map/draw-control.tsx rename to frontend/src/components/map/controls/draw-control.tsx index 4a66d051..f68d29fe 100644 --- a/frontend/src/components/map/draw-control.tsx +++ b/frontend/src/components/map/controls/draw-control.tsx @@ -2,11 +2,17 @@ import { DrawingModes, ToolTipPlacement } from "@/enums"; import { useCallback } from "react"; import { ToolTip } from "@/components/ui/tooltip"; import { PenIcon } from "@/components/ui/icons"; -import { useMap } from "@/app/providers/map-provider"; - -const DrawControl = () => { - const { drawingMode, terraDraw, setDrawingMode } = useMap(); +import { TerraDraw } from "terra-draw"; +export const DrawControl = ({ + drawingMode, + terraDraw, + setDrawingMode, +}: { + drawingMode: DrawingModes; + terraDraw?: TerraDraw; + setDrawingMode: (newMode: DrawingModes) => void; +}) => { const changeMode = useCallback( (newMode: DrawingModes) => { terraDraw?.setMode(newMode); @@ -58,5 +64,3 @@ const DrawControl = () => { ); }; - -export default DrawControl; diff --git a/frontend/src/components/map/controls/fit-to-bounds-control.tsx b/frontend/src/components/map/controls/fit-to-bounds-control.tsx new file mode 100644 index 00000000..7b254146 --- /dev/null +++ b/frontend/src/components/map/controls/fit-to-bounds-control.tsx @@ -0,0 +1,16 @@ +import { ToolTip } from "@/components/ui/tooltip"; +import { FullScreenIcon } from "@/components/ui/icons"; +import { mapContents } from "@/constants"; + +export const FitToBounds = ({ onClick }: { onClick: () => void }) => { + return ( + + + + ); +}; diff --git a/frontend/src/components/map/geolocation-control.tsx b/frontend/src/components/map/controls/geolocation-control.tsx similarity index 86% rename from frontend/src/components/map/geolocation-control.tsx rename to frontend/src/components/map/controls/geolocation-control.tsx index 09d198e4..f06806d1 100644 --- a/frontend/src/components/map/geolocation-control.tsx +++ b/frontend/src/components/map/controls/geolocation-control.tsx @@ -1,11 +1,12 @@ import { GeolocationIcon } from "@/components/ui/icons"; -import { Map } from "maplibre-gl"; import { useCallback } from "react"; import { ToolTip } from "@/components/ui/tooltip"; import { ToolTipPlacement } from "@/enums"; -import { showErrorToast, showWarningToast, TOAST_NOTIFICATIONS } from "@/utils"; +import { showErrorToast, showWarningToast } from "@/utils"; +import { TOAST_NOTIFICATIONS } from "@/constants"; +import { Map } from "maplibre-gl"; -const GeolocationControl = ({ map }: { map: Map | null }) => { +export const GeolocationControl = ({ map }: { map: Map | null }) => { const handleGeolocationClick = useCallback(() => { if (!map) return; @@ -41,5 +42,3 @@ const GeolocationControl = ({ map }: { map: Map | null }) => { ); }; - -export default GeolocationControl; diff --git a/frontend/src/components/map/controls/index.ts b/frontend/src/components/map/controls/index.ts new file mode 100644 index 00000000..750d8444 --- /dev/null +++ b/frontend/src/components/map/controls/index.ts @@ -0,0 +1,7 @@ +export { ZoomControls } from "./zoom-control"; +export { DrawControl } from "./draw-control"; +export { ZoomLevel } from "./current-zoom-control"; +export { LayerControl } from "./layer-control"; +export { MapCursorToolTip } from "./map-cursor-tooltip"; +export { Legend } from "./legend-control"; +export { FitToBounds } from "./fit-to-bounds-control"; diff --git a/frontend/src/components/map/layer-control.tsx b/frontend/src/components/map/controls/layer-control.tsx similarity index 89% rename from frontend/src/components/map/layer-control.tsx rename to frontend/src/components/map/controls/layer-control.tsx index fe34b4b6..283419f7 100644 --- a/frontend/src/components/map/layer-control.tsx +++ b/frontend/src/components/map/controls/layer-control.tsx @@ -3,14 +3,14 @@ import { DropDown } from "@/components/ui/dropdown"; import { useDropdownMenu } from "@/hooks/use-dropdown-menu"; import { Map } from "maplibre-gl"; import { useEffect, useState } from "react"; -import { CheckboxGroup } from "../ui/form"; -import { ToolTip } from "../ui/tooltip"; +import { CheckboxGroup } from "../../ui/form"; +import { ToolTip } from "../../ui/tooltip"; import { BASEMAPS, ToolTipPlacement } from "@/enums"; type TLayers = { id?: string; subLayers: string[]; value: string }[]; type TBasemaps = { id?: string; subLayer: string; value: string }[]; -const LayerControl = ({ +export const LayerControl = ({ map, layers, basemaps, @@ -115,14 +115,16 @@ const LayerControl = ({ distance={10} >
-

Basemap

{basemaps.length > 0 && ( - + <> +

Basemap

+ + )} {layers.length > 0 && ( <> @@ -143,5 +145,3 @@ const LayerControl = ({ ); }; - -export default LayerControl; diff --git a/frontend/src/components/map/controls/legend-control.tsx b/frontend/src/components/map/controls/legend-control.tsx new file mode 100644 index 00000000..eb9e5cab --- /dev/null +++ b/frontend/src/components/map/controls/legend-control.tsx @@ -0,0 +1,65 @@ +import { useState } from "react"; +import { LegendBookIcon } from "../../ui/icons"; +import { LEGEND_NAME_MAPPING, MAP_STYLES_PREFIX } from "@/utils"; +import { Map } from "maplibre-gl"; + +const FillLegendStyle = ({ + fillColor, + fillOpacity, +}: { + fillColor: string; + fillOpacity: number; +}) => { + return ( + + ); +}; + +export const Legend = ({ map }: { map: Map | null }) => { + const [expandLegend, setExpandLegend] = useState(true); + + const activeLayers = map + ?.getStyle() + .layers?.filter( + (layer) => + layer.id.includes(MAP_STYLES_PREFIX) && + layer.layout?.visibility === "visible", + ); + + return ( + + ); +}; diff --git a/frontend/src/components/map/map-cursor-tooltip.tsx b/frontend/src/components/map/controls/map-cursor-tooltip.tsx similarity index 81% rename from frontend/src/components/map/map-cursor-tooltip.tsx rename to frontend/src/components/map/controls/map-cursor-tooltip.tsx index feb11580..2fa79571 100644 --- a/frontend/src/components/map/map-cursor-tooltip.tsx +++ b/frontend/src/components/map/controls/map-cursor-tooltip.tsx @@ -1,12 +1,12 @@ -const MapCursorToolTip = ({ +export const MapCursorToolTip = ({ color = "bg-black", - tooltipVisible, + tooltipVisible = true, tooltipPosition, children, }: { color?: string; tooltipPosition: Record; - tooltipVisible: boolean; + tooltipVisible?: boolean; children: React.ReactNode; }) => { if (!tooltipVisible) return null; @@ -23,5 +23,3 @@ const MapCursorToolTip = ({
); }; - -export default MapCursorToolTip; diff --git a/frontend/src/components/map/zoom-controls.tsx b/frontend/src/components/map/controls/zoom-control.tsx similarity index 57% rename from frontend/src/components/map/zoom-controls.tsx rename to frontend/src/components/map/controls/zoom-control.tsx index c94c2219..33d051dd 100644 --- a/frontend/src/components/map/zoom-controls.tsx +++ b/frontend/src/components/map/controls/zoom-control.tsx @@ -1,8 +1,8 @@ import { cn } from "@/utils"; -import { Map } from "maplibre-gl"; -import { useCallback, useEffect, useState } from "react"; -import { ToolTip } from "../ui/tooltip"; +import { useCallback } from "react"; +import { ToolTip } from "../../ui/tooltip"; import { ToolTipPlacement } from "@/enums"; +import { Map } from "maplibre-gl"; const ZoomButton = ({ onClick, @@ -22,52 +22,41 @@ const ZoomButton = ({ ); -const ZoomControls = ({ map }: { map: Map | null }) => { - const [zoomLevel, setZoomLevel] = useState(null); - - useEffect(() => { - if (!map) return; - setZoomLevel(map.getZoom()); - const handleZoomChange = () => setZoomLevel(map.getZoom()); - map.on("zoomend", handleZoomChange); - return () => { - map.off("zoomend", handleZoomChange); - }; - }, [map]); - +export const ZoomControls = ({ + map, + currentZoom, +}: { + map: Map | null; + currentZoom: number; +}) => { const handleZoomIn = useCallback(() => { - if (map && zoomLevel !== null && zoomLevel < map.getMaxZoom()) { + if (map && currentZoom < map.getMaxZoom()) { map.zoomIn(); } - }, [map, zoomLevel]); + }, [map, currentZoom]); const handleZoomOut = useCallback(() => { - if (map && zoomLevel !== null && zoomLevel > map.getMinZoom()) { + if (map && currentZoom > map.getMinZoom()) { map.zoomOut(); } - }, [map, zoomLevel]); - - if (!map) return null; + }, [map, currentZoom]); return (
= Number(map?.getMaxZoom())} icon="+" /> - {" "}
); }; - -export default ZoomControls; diff --git a/frontend/src/components/map/index.ts b/frontend/src/components/map/index.ts index 3a3aa324..22d2caad 100644 --- a/frontend/src/components/map/index.ts +++ b/frontend/src/components/map/index.ts @@ -1,6 +1,3 @@ -export { default as MapComponent } from "./map"; -export { default as ZoomControls } from "./zoom-controls"; -export { default as DrawControl } from "./draw-control"; -export { default as ZoomLevel } from "./zoom-level"; -export { default as LayerControl } from "./layer-control"; -export { default as MapCursorToolTip } from "./map-cursor-tooltip"; +export { MapComponent } from "./map"; +export * from "./controls"; +export * from "./layers"; diff --git a/frontend/src/components/map/layers/basemaps.tsx b/frontend/src/components/map/layers/basemaps.tsx new file mode 100644 index 00000000..17333677 --- /dev/null +++ b/frontend/src/components/map/layers/basemaps.tsx @@ -0,0 +1,40 @@ +import { useMapLayers } from "@/hooks/use-map-layer"; +import { + GOOGLE_SATELLITE_BASEMAP_LAYER_ID, + GOOGLE_SATELLITE_BASEMAP_SOURCE_ID, +} from "@/utils"; +import { Map } from "maplibre-gl"; + +export const Basemaps = ({ map }: { map: Map | null }) => { + useMapLayers( + [ + { + id: GOOGLE_SATELLITE_BASEMAP_LAYER_ID, + type: "raster", + source: GOOGLE_SATELLITE_BASEMAP_SOURCE_ID, + layout: { visibility: "none" }, + minzoom: 0, + maxzoom: 22, + }, + ], + [ + { + id: GOOGLE_SATELLITE_BASEMAP_SOURCE_ID, + spec: { + type: "raster", + tiles: [ + "https://mt0.google.com/vt/lyrs=s&x={x}&y={y}&z={z}", + "https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}", + "https://mt2.google.com/vt/lyrs=s&x={x}&y={y}&z={z}", + "https://mt3.google.com/vt/lyrs=s&x={x}&y={y}&z={z}", + ], + attribution: "© Google", + tileSize: 256, + }, + }, + ], + map, + ); + + return null; +}; diff --git a/frontend/src/components/map/layers/index.ts b/frontend/src/components/map/layers/index.ts new file mode 100644 index 00000000..5f6b10f2 --- /dev/null +++ b/frontend/src/components/map/layers/index.ts @@ -0,0 +1,3 @@ +export { TileBoundaries } from "./tile-boundaries"; +export { OpenAerialMap } from "./open-aerial-map"; +export { Basemaps } from "./basemaps"; diff --git a/frontend/src/components/map/layers/open-aerial-map.tsx b/frontend/src/components/map/layers/open-aerial-map.tsx new file mode 100644 index 00000000..1256c967 --- /dev/null +++ b/frontend/src/components/map/layers/open-aerial-map.tsx @@ -0,0 +1,34 @@ +import { useMapLayers } from "@/hooks/use-map-layer"; +import { TMS_LAYER_ID, TMS_SOURCE_ID } from "@/utils"; +import { Map } from "maplibre-gl"; + +export const OpenAerialMap = ({ + tileJSONURL, + map, +}: { + tileJSONURL?: string; + map: Map | null; +}) => { + useMapLayers( + [ + { + id: TMS_LAYER_ID, + type: "raster", + source: TMS_SOURCE_ID, + layout: { visibility: "visible" }, + }, + ], + [ + { + id: TMS_SOURCE_ID, + spec: { + type: "raster", + url: tileJSONURL, + tileSize: 256, + }, + }, + ], + map, + ); + return null; +}; diff --git a/frontend/src/components/map/layers/tile-boundaries.tsx b/frontend/src/components/map/layers/tile-boundaries.tsx new file mode 100644 index 00000000..d355c620 --- /dev/null +++ b/frontend/src/components/map/layers/tile-boundaries.tsx @@ -0,0 +1,61 @@ +import { useMapLayers } from "@/hooks/use-map-layer"; +import { GeoJSONType } from "@/types"; +import { + getTileBoundariesGeoJSON, + TILE_BOUNDARY_LAYER_ID, + TILE_BOUNDARY_SOURCE_ID, +} from "@/utils"; +import { GeoJSONSource, Map } from "maplibre-gl"; +import { useCallback, useEffect } from "react"; + +export const TileBoundaries = ({ map }: { map: Map | null }) => { + useMapLayers( + [ + { + id: TILE_BOUNDARY_LAYER_ID, + type: "line", + source: TILE_BOUNDARY_SOURCE_ID, + paint: { + "line-color": "#FFF", + "line-width": 1, + }, + layout: { visibility: "visible" }, + }, + ], + [ + { + id: TILE_BOUNDARY_SOURCE_ID, + spec: { + type: "geojson", + data: { type: "FeatureCollection", features: [] }, + }, + }, + ], + map, + ); + + const updateTileBoundary = useCallback(() => { + if (map) { + if (map.getSource(TILE_BOUNDARY_SOURCE_ID)) { + const tileBoundaries = getTileBoundariesGeoJSON( + map, + // There is a mismatch of 1 in the mag.getZoom() results and the actual zoom level of the map. + // Adding 1 to the result resolves it. + Math.round(map.getZoom() + 1), + ); + const source = map.getSource(TILE_BOUNDARY_SOURCE_ID) as GeoJSONSource; + source.setData(tileBoundaries as GeoJSONType); + } + } + }, [map]); + + useEffect(() => { + if (!map) return; + map.on("moveend", updateTileBoundary); + return () => { + map.off("moveend", updateTileBoundary); + }; + }, [map, updateTileBoundary]); + + return null; +}; diff --git a/frontend/src/components/map/map.tsx b/frontend/src/components/map/map.tsx index b9804d52..09c3250f 100644 --- a/frontend/src/components/map/map.tsx +++ b/frontend/src/components/map/map.tsx @@ -1,18 +1,29 @@ -import { useEffect, useRef } from "react"; +import { + GOOGLE_SATELLITE_BASEMAP_LAYER_ID, + OSM_BASEMAP_LAYER_ID, + TMS_LAYER_ID, +} from "@/utils"; import "maplibre-gl/dist/maplibre-gl.css"; -import { MAP_STYLES } from "@/utils"; -import ZoomControls from "./zoom-controls"; -import GeolocationControl from "./geolocation-control"; -import DrawControl from "./draw-control"; -import ZoomLevel from "./zoom-level"; -import LayerControl from "./layer-control"; -import { useMap } from "@/app/providers/map-provider"; -import { setupMaplibreMap } from "./setup-maplibre"; -import { BASEMAPS } from "@/enums"; +import { RefObject, useMemo } from "react"; +import { BASEMAPS, DrawingModes } from "@/enums"; + +import { ZoomControls } from "@/components/map/controls/zoom-control"; +import { GeolocationControl } from "@/components/map/controls/geolocation-control"; +import { DrawControl } from "@/components/map/controls/draw-control"; +import { ZoomLevel } from "@/components/map/controls/current-zoom-control"; +import { LayerControl } from "@/components/map/controls/layer-control"; +import { Legend } from "@/components/map/controls/legend-control"; +import { TileBoundaries } from "@/components/map/layers/tile-boundaries"; +import { OpenAerialMap } from "@/components/map/layers/open-aerial-map"; +import { Basemaps } from "@/components/map/layers/basemaps"; +import { ControlsPosition } from "@/enums"; +import { LngLatBoundsLike, Map } from "maplibre-gl"; +import { FitToBounds } from "./controls"; +import { TerraDraw } from "terra-draw"; type MapComponentProps = { geolocationControl?: boolean; - controlsLocation?: "top-right" | "top-left"; + controlsPosition?: ControlsPosition; drawControl?: boolean; showCurrentZoom?: boolean; layerControl?: boolean; @@ -20,68 +31,136 @@ type MapComponentProps = { value: string; subLayers: string[]; }[]; - layerControlBasemaps?: { - value: string; - subLayer: string; - }[]; + showTileBoundary?: boolean; children?: React.ReactNode; + showLegend?: boolean; + openAerialMap?: boolean; + oamTileJSONURL?: string; + basemaps?: boolean; + fitToBounds?: boolean; + bounds?: LngLatBoundsLike; + // layers?: LayerSpecification[] + // sources?: { id: string; spec: SourceSpecification }[], + onMapLoad?: (map: Map) => void; + mapContainerRef?: RefObject | null; + map: Map | null; + terraDraw?: TerraDraw | undefined; + currentZoom?: number; + drawingMode?: DrawingModes; + setDrawingMode?: (newMode: DrawingModes) => void; }; -const MapComponent: React.FC = ({ +export const MapComponent: React.FC = ({ geolocationControl = false, - controlsLocation = "top-right", + controlsPosition = ControlsPosition.TOP_RIGHT, drawControl = false, showCurrentZoom = false, layerControl = false, layerControlLayers = [], - layerControlBasemaps = [], + showTileBoundary = false, + showLegend = false, + openAerialMap = false, + oamTileJSONURL, + basemaps = false, children, + fitToBounds, + bounds, + mapContainerRef, + map, + terraDraw, + currentZoom, + drawingMode, + setDrawingMode, }) => { - const mapContainerRef = useRef(null); - const { map, setMap, terraDraw } = useMap(); + const layerControlData = useMemo(() => { + const layers = [ + ...layerControlLayers, + ...(openAerialMap + ? [{ value: "TMS Layer", subLayers: [TMS_LAYER_ID] }] + : []), + ]; + const baseLayers = basemaps + ? [ + { value: BASEMAPS.OSM, subLayer: OSM_BASEMAP_LAYER_ID }, + { + value: BASEMAPS.GOOGLE_SATELLITE, + subLayer: GOOGLE_SATELLITE_BASEMAP_LAYER_ID, + }, + ] + : []; + return { layers, baseLayers }; + }, [layerControlLayers, openAerialMap, basemaps]); - useEffect(() => { - const maplibreMap = setupMaplibreMap( - mapContainerRef, - MAP_STYLES[BASEMAPS.OSM], + const Controls = useMemo(() => { + if (!map) return; + return ( + <> +
+ {currentZoom ? ( + + ) : null} + {geolocationControl && } + {drawControl && terraDraw && drawingMode && setDrawingMode && ( + + )} +
+
+ {showCurrentZoom && currentZoom ? ( + + ) : null} + {layerControl && ( + + )} +
+ ); - maplibreMap.on("load", () => { - setMap(maplibreMap); - }); - }, []); + }, [ + map, + geolocationControl, + drawControl, + terraDraw, + controlsPosition, + layerControl, + showCurrentZoom, + layerControlData, + currentZoom, + drawingMode && setDrawingMode, + ]); return ( -
-
- {map && ( - <> - - {geolocationControl && } - {drawControl && terraDraw && } - - )} -
-
- {map && ( - <> - {showCurrentZoom && } - {layerControl && ( - - )} - - )} -
+
+ {Controls} + {map && showLegend && } + {/* Order according to how they'll be rendered */} + {basemaps && } + {openAerialMap && oamTileJSONURL && ( + + )} + {showTileBoundary && } + {fitToBounds && map && ( + + map?.fitBounds(bounds as LngLatBoundsLike, { padding: 10 }) + } + /> + )} {children}
); }; - -export default MapComponent; diff --git a/frontend/src/components/map/setup-maplibre.ts b/frontend/src/components/map/setups/setup-maplibre.ts similarity index 78% rename from frontend/src/components/map/setup-maplibre.ts rename to frontend/src/components/map/setups/setup-maplibre.ts index af89f4f8..8d2070f4 100644 --- a/frontend/src/components/map/setup-maplibre.ts +++ b/frontend/src/components/map/setups/setup-maplibre.ts @@ -1,10 +1,11 @@ import { MAX_ZOOM_LEVEL } from "@/utils"; import maplibregl, { Map, StyleSpecification } from "maplibre-gl"; +import { Protocol } from "pmtiles"; -export function setupMaplibreMap( +export const setupMaplibreMap = ( containerRef: React.RefObject, style: StyleSpecification | string, -): Map { +): Map => { // Check if RTL plugin is needed and set it if (maplibregl.getRTLTextPluginStatus() === "unavailable") { maplibregl.setRTLTextPlugin( @@ -13,6 +14,9 @@ export function setupMaplibreMap( ); } + let protocol = new Protocol(); + maplibregl.addProtocol("pmtiles", protocol.tile); + return new maplibregl.Map({ container: containerRef.current!, style: style, @@ -21,4 +25,4 @@ export function setupMaplibreMap( minZoom: 1, maxZoom: MAX_ZOOM_LEVEL, }); -} +}; diff --git a/frontend/src/components/map/setup-terra-draw.ts b/frontend/src/components/map/setups/setup-terra-draw.ts similarity index 96% rename from frontend/src/components/map/setup-terra-draw.ts rename to frontend/src/components/map/setups/setup-terra-draw.ts index a87f4228..653b0b5d 100644 --- a/frontend/src/components/map/setup-terra-draw.ts +++ b/frontend/src/components/map/setups/setup-terra-draw.ts @@ -12,7 +12,7 @@ import { TRAINING_AREAS_AOI_OUTLINE_WIDTH, } from "@/utils"; -export function setupTerraDraw(map: maplibregl.Map) { +export const setupTerraDraw = (map: maplibregl.Map) => { return new TerraDraw({ tracked: true, adapter: new TerraDrawMapLibreGLAdapter({ @@ -67,4 +67,4 @@ export function setupTerraDraw(map: maplibregl.Map) { }), ], }); -} +}; diff --git a/frontend/src/components/map/zoom-level.tsx b/frontend/src/components/map/zoom-level.tsx deleted file mode 100644 index 5b4ebaa0..00000000 --- a/frontend/src/components/map/zoom-level.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { useMap } from "@/app/providers/map-provider"; -import { roundNumber } from "@/utils"; - -const ZoomLevel = () => { - const { currentZoom } = useMap(); - - return ( -
-

Zoom level: {roundNumber(currentZoom, 0)}

-
- ); -}; - -export default ZoomLevel; diff --git a/frontend/src/components/pagination.tsx b/frontend/src/components/pagination.tsx deleted file mode 100644 index 9c6b49d4..00000000 --- a/frontend/src/components/pagination.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import ChevronDownIcon from "@/components/ui/icons/chevron-down-icon"; -import { SEARCH_PARAMS } from "@/app/routes/models/models-list"; -import { TQueryParams } from "@/types"; - -export const PAGE_LIMIT = 20; - -type PaginationProps = { - hasNextPage?: boolean; - hasPrevPage?: boolean; - disableNextPage: boolean; - disablePrevPage: boolean; - totalLength?: number; - pageLimit: number; - query?: TQueryParams; - updateQuery?: (params: TQueryParams) => void; - isPlaceholderData?: boolean; - offset?: number; - setOffset?: (offset: number) => void; -}; - -const Pagination: React.FC = ({ - hasNextPage, - hasPrevPage, - disableNextPage, - totalLength = 0, - disablePrevPage, - pageLimit, - query, - updateQuery, - isPlaceholderData, - offset, - setOffset, -}) => { - const _offset = offset ?? (query?.[SEARCH_PARAMS.offset] as number); - - const onNextPage = () => { - if (!isPlaceholderData && hasNextPage) { - const nextOffset = _offset + pageLimit; - updateQuery?.({ - [SEARCH_PARAMS.offset]: _offset + pageLimit, - }); - setOffset?.(nextOffset); - } - }; - - const onPrevPage = () => { - if (hasPrevPage) { - const prevOffset = _offset - pageLimit; - updateQuery?.({ - [SEARCH_PARAMS.offset]: Math.max(prevOffset, 0), - }); - setOffset?.(Math.max(prevOffset, 0)); - } - }; - - return ( -
-

- - {_offset + 1} -{" "} - {_offset + pageLimit < (totalLength ? totalLength : 0) - ? _offset + pageLimit - : totalLength} - {" "} - of {totalLength} -

-
- - -
-
- ); -}; - -export default Pagination; diff --git a/frontend/src/components/seo/head.tsx b/frontend/src/components/seo/head.tsx index 7530d6d6..7dd0e234 100644 --- a/frontend/src/components/seo/head.tsx +++ b/frontend/src/components/seo/head.tsx @@ -8,7 +8,11 @@ type HeadProps = { export const Head = ({ title = "", description = "" }: HeadProps = {}) => { return ( - {title ? `${title} | fAIr` : undefined} + + {title + ? `${title} | fAIr | Humanitarian OpenStreetMap Team (HOT)` + : undefined} + ); diff --git a/frontend/src/components/landing/fair-process/fair-process.module.css b/frontend/src/components/shared/fair-process/fair-process.module.css similarity index 71% rename from frontend/src/components/landing/fair-process/fair-process.module.css rename to frontend/src/components/shared/fair-process/fair-process.module.css index c71774b1..43376dc9 100644 --- a/frontend/src/components/landing/fair-process/fair-process.module.css +++ b/frontend/src/components/shared/fair-process/fair-process.module.css @@ -6,6 +6,6 @@ /* lg: */ @media (min-width: 1024px) { .fairProcess { - padding: 100px var(--hot-fair-spacing-5x-large); + padding: 100px var(--hot-fair-spacing-extra-large); } -} +} \ No newline at end of file diff --git a/frontend/src/components/landing/fair-process/fair-process.tsx b/frontend/src/components/shared/fair-process/fair-process.tsx similarity index 94% rename from frontend/src/components/landing/fair-process/fair-process.tsx rename to frontend/src/components/shared/fair-process/fair-process.tsx index 2e5b284f..c3d614d1 100644 --- a/frontend/src/components/landing/fair-process/fair-process.tsx +++ b/frontend/src/components/shared/fair-process/fair-process.tsx @@ -9,14 +9,14 @@ import styles from "./fair-process.module.css"; import { AnimatedBeam } from "@/components/ui/animated-beam"; import { BotIcon, FeedbackIcon, PredictionsIcon } from "@/components/ui/icons"; import { IconProps } from "@/types"; -import DesktopFlowIcon from "@/components/ui/icons/desktop-flow-icon"; +import { DesktopFlowIcon } from "@/components/ui/icons"; import { APP_CONTENT } from "@/utils/content"; /** * The delay in seconds before switching to the next step. This can be adjust accordingly. * The lower it is, the longer time it takes before the beam animates from the origin node to the destination node. */ -const AUTOSCROLL_DELAY: number = 3500; +const AUTOSCROLL_DELAY: number = 2500; type TSteps = { title: string; @@ -47,7 +47,11 @@ const steps: TSteps[] = [ }, ]; -const TheFAIRProcess = () => { +const TheFAIRProcess = ({ + disableStyle = false, +}: { + disableStyle?: boolean; +}) => { const containerRef = useRef(null); const itemRefs = useRef>>( steps.map(() => React.createRef()), @@ -85,7 +89,7 @@ const TheFAIRProcess = () => { return ( { }, [steps.length, activeIndex]); return ( -
+

{APP_CONTENT.homepage.fairProcess.title}

diff --git a/frontend/src/components/landing/faqs/faqs.module.css b/frontend/src/components/shared/faqs/faqs.module.css similarity index 78% rename from frontend/src/components/landing/faqs/faqs.module.css rename to frontend/src/components/shared/faqs/faqs.module.css index afbba4d8..4379cac7 100644 --- a/frontend/src/components/landing/faqs/faqs.module.css +++ b/frontend/src/components/shared/faqs/faqs.module.css @@ -5,13 +5,13 @@ .heading { width: auto; text-align: center; - font-size: var(--hot-fair-font-size-title-2); + font-size: var(--hot-fair-font-size-title-4); font-weight: var(--hot-fair-font-weight-bold); text-transform: uppercase; background-color: var(--hot-fair-color-primary); color: var(--hot-fair-color-white); display: inline-block; - padding: var(--sl-spacing-x-small) var(--sl-spacing-medium); + padding: var(--sl-spacing-x-small) var(--sl-spacing-2x-large); } .seeMore { @@ -32,12 +32,12 @@ /* md: */ @media (min-width: 768px) { .FAQS { - padding: 0 var(--hot-fair-spacing-5x-large); + padding: 0 var(--hot-fair-spacing-extra-large); } .heading { max-width: 218px; - font-size: var(--hot-fair-font-size-title-1); + font-size: var(--hot-fair-font-size-title-2); font-weight: var(--hot-fair-font-weight-semibold); } @@ -45,4 +45,4 @@ margin: 0 auto; max-width: 844px; } -} +} \ No newline at end of file diff --git a/frontend/src/components/shared/faqs/faqs.tsx b/frontend/src/components/shared/faqs/faqs.tsx new file mode 100644 index 00000000..db53b9a1 --- /dev/null +++ b/frontend/src/components/shared/faqs/faqs.tsx @@ -0,0 +1,50 @@ +import styles from "./faqs.module.css"; +import { APP_CONTENT } from "@/utils/content"; +import { Accordion } from "@/components/ui/accordion"; +import { Link } from "@/components/ui/link"; +import { APPLICATION_ROUTES } from "@/utils"; +import { TFAQs } from "@/types"; +import React from "react"; +import { ChevronDownIcon } from "@/components/ui/icons"; + +export const FAQs = React.memo( + ({ + faqs = APP_CONTENT.homepage.faqs.content, + disableSeeMoreButton, + }: { + faqs?: TFAQs; + disableSeeMoreButton?: boolean; + }) => { + return ( +
+

+ {APP_CONTENT.homepage.faqs.sectionTitle} +

+
+
+ {faqs.map((faq, id) => ( + + ))} +
+ {!disableSeeMoreButton && ( + +

+ {APP_CONTENT.homepage.faqs.cta} + +

+ + )} +
+
+ ); + }, +); diff --git a/frontend/src/components/shared/header/header.module.css b/frontend/src/components/shared/header/header.module.css new file mode 100644 index 00000000..0e639ba1 --- /dev/null +++ b/frontend/src/components/shared/header/header.module.css @@ -0,0 +1,3 @@ +.headerBackground { + background-image: url("../../../src/assets/svgs/header_bg_contour.svg"); +} diff --git a/frontend/src/components/shared/header/header.tsx b/frontend/src/components/shared/header/header.tsx new file mode 100644 index 00000000..67a92d2f --- /dev/null +++ b/frontend/src/components/shared/header/header.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import styles from "./header.module.css"; + +export const Header = React.memo(({ title }: { title: string }) => { + return ( +
+
+ {title} + +
+
+ +
+
+ ); +}); + +const Rectangles = React.memo(() => { + return ( +
+
+
+
+
+
+ ); +}); diff --git a/frontend/src/components/shared/index.ts b/frontend/src/components/shared/index.ts new file mode 100644 index 00000000..1ac79a69 --- /dev/null +++ b/frontend/src/components/shared/index.ts @@ -0,0 +1,3 @@ +export { Header } from "./header/header"; +export { FAQs } from "./faqs/faqs"; +export { SectionHeader } from "./section-header/section-header"; diff --git a/frontend/src/components/shared/pagination.tsx b/frontend/src/components/shared/pagination.tsx new file mode 100644 index 00000000..2d7ac0c1 --- /dev/null +++ b/frontend/src/components/shared/pagination.tsx @@ -0,0 +1,113 @@ +import { ChevronDownIcon } from "@/components/ui/icons"; +import { SEARCH_PARAMS } from "@/app/routes/models/models-list"; +import { TQueryParams } from "@/types"; +import { useScrollToTop } from "@/hooks/use-scroll-to-element"; + +export const PAGE_LIMIT = 20; + +type PaginationProps = { + hasNextPage?: boolean; + hasPrevPage?: boolean; + disableNextPage: boolean; + disablePrevPage: boolean; + totalLength?: number; + pageLimit: number; + query?: TQueryParams; + updateQuery?: (params: TQueryParams) => void; + isPlaceholderData?: boolean; + offset?: number; + setOffset?: (offset: number) => void; + showCountOnMobile?: boolean; + centerOnMobile?: boolean; +}; + +const Pagination: React.FC = ({ + hasNextPage, + hasPrevPage, + disableNextPage, + totalLength = 0, + disablePrevPage, + pageLimit, + query, + updateQuery, + isPlaceholderData, + offset, + setOffset, + showCountOnMobile = false, + centerOnMobile = true, +}) => { + const _offset = offset ?? (query?.[SEARCH_PARAMS.offset] as number); + const { scrollToTop } = useScrollToTop(); + const onNextPage = () => { + if (!isPlaceholderData && hasNextPage) { + const nextOffset = _offset + pageLimit; + updateQuery?.({ + [SEARCH_PARAMS.offset]: _offset + pageLimit, + }); + setOffset?.(nextOffset); + // scroll to top + scrollToTop(); + } + }; + + const onPrevPage = () => { + if (hasPrevPage) { + const prevOffset = _offset - pageLimit; + updateQuery?.({ + [SEARCH_PARAMS.offset]: Math.max(prevOffset, 0), + }); + setOffset?.(Math.max(prevOffset, 0)); + // scroll to top + scrollToTop(); + } + }; + + return ( +
+
+

+ + {_offset + 1} -{" "} + {_offset + pageLimit < (totalLength ? totalLength : 0) + ? _offset + pageLimit + : totalLength} + {" "} + + {" "} + of {totalLength} + +

+
+
+
+ + +
+
+
+ ); +}; + +export default Pagination; diff --git a/frontend/src/components/shared/section-header/section-header.tsx b/frontend/src/components/shared/section-header/section-header.tsx new file mode 100644 index 00000000..7a768cfa --- /dev/null +++ b/frontend/src/components/shared/section-header/section-header.tsx @@ -0,0 +1,9 @@ +import React from "react"; + +export const SectionHeader = React.memo(({ title }: { title: string }) => { + return ( +

+ {title} +

+ ); +}); diff --git a/frontend/src/components/ui/accordion/accordion.tsx b/frontend/src/components/ui/accordion/accordion.tsx index b67129bf..ed43d68a 100644 --- a/frontend/src/components/ui/accordion/accordion.tsx +++ b/frontend/src/components/ui/accordion/accordion.tsx @@ -1,5 +1,5 @@ import { SlDetails } from "@shoelace-style/shoelace/dist/react"; -import ChevronDownIcon from "@/components/ui/icons/chevron-down-icon"; +import { ChevronDownIcon } from "@/components/ui/icons"; import "./accordion.css"; type AccordionProps = { diff --git a/frontend/src/components/ui/button/back-button.tsx b/frontend/src/components/ui/button/back-button.tsx index 577afe26..35ff2e37 100644 --- a/frontend/src/components/ui/button/back-button.tsx +++ b/frontend/src/components/ui/button/back-button.tsx @@ -1,16 +1,16 @@ import { useNavigate } from "react-router-dom"; import { ArrowBackIcon } from "@/components/ui/icons"; -const BackButton = () => { +const BackButton = ({ className }: { className?: string }) => { const navigate = useNavigate(); return ( ); }; diff --git a/frontend/src/components/ui/button/button.tsx b/frontend/src/components/ui/button/button.tsx index d66d5c6b..b0a1fef3 100644 --- a/frontend/src/components/ui/button/button.tsx +++ b/frontend/src/components/ui/button/button.tsx @@ -1,4 +1,4 @@ -import SlButton from "@shoelace-style/shoelace/dist/react/button/index.js"; +import { SlButton } from "@shoelace-style/shoelace/dist/react"; import "./button.css"; import { Spinner } from "@/components/ui/spinner"; import { cn } from "@/utils"; diff --git a/frontend/src/components/ui/button/icon-button.tsx b/frontend/src/components/ui/button/icon-button.tsx index 13d60932..dd72ba8b 100644 --- a/frontend/src/components/ui/button/icon-button.tsx +++ b/frontend/src/components/ui/button/icon-button.tsx @@ -8,7 +8,7 @@ type ButtonWithIconProps = { variant: ButtonVariant; prefixIcon?: React.ElementType; suffixIcon?: React.ElementType; - capitalizeText?: boolean; + className?: string; iconClassName?: string; disabled?: boolean; diff --git a/frontend/src/components/ui/codeblock/codeblock.tsx b/frontend/src/components/ui/codeblock/codeblock.tsx index 8a096293..566d4062 100644 --- a/frontend/src/components/ui/codeblock/codeblock.tsx +++ b/frontend/src/components/ui/codeblock/codeblock.tsx @@ -1,6 +1,6 @@ const CodeBlock = ({ content }: { content: string }) => { return ( -
+
         {content}
       
diff --git a/frontend/src/components/ui/dialog/dialog.tsx b/frontend/src/components/ui/dialog/dialog.tsx index 45490f09..f3b91c44 100644 --- a/frontend/src/components/ui/dialog/dialog.tsx +++ b/frontend/src/components/ui/dialog/dialog.tsx @@ -1,4 +1,4 @@ -import SlDialog from "@shoelace-style/shoelace/dist/react/dialog/index.js"; +import { SlDialog } from "@shoelace-style/shoelace/dist/react"; import "./dialog.css"; import { SHOELACE_SIZES } from "@/enums"; import useScreenSize from "@/hooks/use-screen-size"; diff --git a/frontend/src/components/ui/drawer/drawer.tsx b/frontend/src/components/ui/drawer/drawer.tsx index 420ddba7..ef1577e5 100644 --- a/frontend/src/components/ui/drawer/drawer.tsx +++ b/frontend/src/components/ui/drawer/drawer.tsx @@ -1,12 +1,14 @@ -import SlDrawer from "@shoelace-style/shoelace/dist/react/drawer/index.js"; +import { SlDrawer } from "@shoelace-style/shoelace/dist/react"; import "./drawer.css"; +import { DrawerPlacements } from "@/enums"; type DrawerProps = { open: boolean; setOpen: (open: boolean) => void; - placement: "top" | "bottom" | "end"; + placement: DrawerPlacements; children: React.ReactNode; label?: string; + noHeader?: boolean; }; const Drawer: React.FC = ({ children, @@ -14,14 +16,18 @@ const Drawer: React.FC = ({ setOpen, placement, label = "", + noHeader = true, }) => { return ( setOpen(false)} - noHeader + onSlAfterHide={(e) => { + setOpen(false); + e.stopPropagation(); + }} + noHeader={noHeader} > {children} diff --git a/frontend/src/components/ui/dropdown/dropdown.tsx b/frontend/src/components/ui/dropdown/dropdown.tsx index 213929e4..a4edd895 100644 --- a/frontend/src/components/ui/dropdown/dropdown.tsx +++ b/frontend/src/components/ui/dropdown/dropdown.tsx @@ -1,9 +1,9 @@ -import SlDropdown from "@shoelace-style/shoelace/dist/react/dropdown/index.js"; -import SlMenu from "@shoelace-style/shoelace/dist/react/menu/index.js"; -import SlMenuItem from "@shoelace-style/shoelace/dist/react/menu-item/index.js"; -import SlCheckbox from "@shoelace-style/shoelace/dist/react/checkbox/index.js"; +import { SlDropdown } from "@shoelace-style/shoelace/dist/react"; +import { SlMenu } from "@shoelace-style/shoelace/dist/react"; +import { SlMenuItem } from "@shoelace-style/shoelace/dist/react"; +import { SlCheckbox } from "@shoelace-style/shoelace/dist/react"; import "./dropdown.css"; -import ChevronDownIcon from "../icons/chevron-down-icon"; +import { ChevronDownIcon } from "@/components/ui/icons"; import { useEffect, useState } from "react"; import { cn } from "@/utils"; @@ -13,7 +13,7 @@ export type DropdownMenuItem = { className?: string; name?: string; disabled?: boolean; - apiValue?: string; + apiValue?: string | number; }; type DropDownProps = { diff --git a/frontend/src/components/ui/footer/footer.tsx b/frontend/src/components/ui/footer/footer.tsx index e72910fc..31b532de 100644 --- a/frontend/src/components/ui/footer/footer.tsx +++ b/frontend/src/components/ui/footer/footer.tsx @@ -1,9 +1,9 @@ import CreativeCommonsBadge from "@/assets/images/cc_by_badge.png"; -import FacebookLogo from "@/assets/socials/facebook_logo.svg"; -import GitHubLogo from "@/assets/socials/github_logo.svg"; -import XLogo from "@/assets/socials/x_logo.svg"; -import InstagramLogo from "@/assets/socials/instagram_logo.svg"; -import YoutTubeLogo from "@/assets/socials/youtube_logo.svg"; +import FacebookLogo from "@/assets/svgs/socials/facebook_logo.svg"; +import GitHubLogo from "@/assets/svgs/socials/github_logo.svg"; +import XLogo from "@/assets/svgs/socials/x_logo.svg"; +import InstagramLogo from "@/assets/svgs/socials/instagram_logo.svg"; +import YoutTubeLogo from "@/assets/svgs/socials/youtube_logo.svg"; import { APP_CONTENT } from "@/utils/content"; import { Image } from "@/components/ui/image"; import { Link } from "@/components/ui/link"; @@ -38,7 +38,7 @@ const socials = [ const Footer = () => { return (