diff --git a/api/package.json b/api/package.json index 428969f..a086457 100644 --- a/api/package.json +++ b/api/package.json @@ -51,6 +51,7 @@ "@nestjs/testing": "^10.0.0", "@types/bcrypt": "^5.0.2", "@types/express": "^4.17.17", + "@types/geojson": "catalog:", "@types/jest": "^29.5.12", "@types/jsonapi-serializer": "^3.6.8", "@types/lodash": "^4.17.7", diff --git a/api/src/modules/countries/map/map.repository.ts b/api/src/modules/countries/map/map.repository.ts index 37f639b..5c51984 100644 --- a/api/src/modules/countries/map/map.repository.ts +++ b/api/src/modules/countries/map/map.repository.ts @@ -1,9 +1,13 @@ import { Injectable, Logger, NotFoundException } from '@nestjs/common'; -import { FeatureCollection, Repository } from 'typeorm'; +import { Repository } from 'typeorm'; import { InjectRepository } from '@nestjs/typeorm'; import { Country } from '@shared/entities/country.entity'; import { BaseData } from '@shared/entities/base-data.entity'; +import { FeatureCollection, Geometry } from 'geojson'; + +import { ProjectGeoProperties } from '@shared/schemas/geometries/projects'; + /** * @description: The aim for this repository is to work with geospatial data, for now "geometry" column in countries * table. The country repository will be used to work with the metadata of the countries and avoid loading geometries when only metadata is needed, @@ -22,7 +26,7 @@ export class MapRepository extends Repository { async getGeoFeatures( countryCode: Country['code'], - ): Promise { + ): Promise> { const queryBuilder = this.createQueryBuilder('country'); queryBuilder.innerJoin(BaseData, 'bd', 'bd.country_code = country.code'); queryBuilder.select( @@ -45,10 +49,13 @@ export class MapRepository extends Repository { }); } - const result: { geojson: FeatureCollection } | undefined = - await queryBuilder.getRawOne<{ - geojson: FeatureCollection; - }>(); + const result: + | { + geojson: FeatureCollection; + } + | undefined = await queryBuilder.getRawOne<{ + geojson: FeatureCollection; + }>(); this.logger.log(`Retrieved geo features`); if (!result) { throw new NotFoundException(`Could not retrieve geo features`); diff --git a/client/package.json b/client/package.json index 8aefdb8..fb5522f 100644 --- a/client/package.json +++ b/client/package.json @@ -45,7 +45,7 @@ }, "devDependencies": { "@types/d3": "7.4.3", - "@types/geojson": "7946.0.14", + "@types/geojson": "catalog:", "@types/mapbox-gl": "3.4.0", "@types/node": "catalog:", "@types/react": "^18", diff --git a/client/src/app/(projects)/url-store.ts b/client/src/app/(projects)/url-store.ts index 0e40831..066c19f 100644 --- a/client/src/app/(projects)/url-store.ts +++ b/client/src/app/(projects)/url-store.ts @@ -1,4 +1,9 @@ -import { parseAsJson, parseAsStringLiteral, useQueryState } from "nuqs"; +import { + parseAsJson, + parseAsString, + parseAsStringLiteral, + useQueryState, +} from "nuqs"; import { z } from "zod"; import { @@ -29,6 +34,10 @@ export function useGlobalFilters() { ); } +export function useSyncCountry() { + return useQueryState("country", parseAsString.withDefault("")); +} + export function useTableMode() { return useQueryState( "table", diff --git a/client/src/containers/projects/map/layers/projects/index.tsx b/client/src/containers/projects/map/layers/projects/index.tsx index d5da406..7489359 100644 --- a/client/src/containers/projects/map/layers/projects/index.tsx +++ b/client/src/containers/projects/map/layers/projects/index.tsx @@ -1,329 +1,91 @@ import { Source, Layer } from "react-map-gl"; import * as d3 from "d3"; -import { FeatureCollection, Geometry } from "geojson"; import { FillLayerSpecification } from "mapbox-gl"; +import { client } from "@/lib/query-client"; +import { geometriesKeys } from "@/lib/query-keys"; + +import { useSyncCountry } from "@/app/(projects)/url-store"; + import { generateColorRamp } from "@/containers/projects/map/layers/projects/utils"; export default function ProjectsLayer() { - const _data: FeatureCollection< - Geometry, + const [country] = useSyncCountry(); + + const queryKey = country + ? geometriesKeys.country(country).queryKey + : geometriesKeys.all.queryKey; + + const { data, isSuccess } = client.map.getGeoFeatures.useQuery( + queryKey, { - cost: number; - abatement: number; - } - > = { - type: "FeatureCollection", - features: [ - { - type: "Feature", - properties: { - cost: 10, - abatement: 10, - }, - geometry: { - coordinates: [ - [ - [-1.0188997350152817, 19.339371896125982], - [-1.0188997350152817, 15.2827299773014], - [4.790209065497407, 15.2827299773014], - [4.790209065497407, 19.339371896125982], - [-1.0188997350152817, 19.339371896125982], - ], - ], - type: "Polygon", - }, - }, - { - type: "Feature", - properties: { - cost: 2, - abatement: 2, - }, - geometry: { - coordinates: [ - [ - [8.553884254294076, 22.347045198308763], - [8.553884254294076, 18.98034659264529], - [13.93901438502084, 18.98034659264529], - [13.93901438502084, 22.347045198308763], - [8.553884254294076, 22.347045198308763], - ], - ], - type: "Polygon", - }, - }, - { - type: "Feature", - properties: { - cost: 3, - abatement: 3, - }, - geometry: { - type: "Polygon", - coordinates: [ - [ - [10.266381087897429, 18.198922150882048], - [10.126800739719087, 18.192359148568762], - [9.988595064490216, 18.172734820447474], - [9.85312422606958, 18.14024251677739], - [9.721719567720436, 18.095202210312237], - [9.595669690810766, 18.038057113269076], - [9.476207106652101, 17.969369007114743], - [9.364495630484392, 17.889812349055255], - [9.261618669117883, 17.800167234433463], - [9.168568533490959, 17.701311307580816], - [9.086236885294658, 17.594210724810743], - [9.015406403773625, 17.479910282070346], - [8.956743735727128, 17.359522826272258], - [8.910793769403016, 17.23421807359045], - [8.877975252072886, 17.10521096018074], - [8.858577752113135, 16.973749651097236], - [8.85275994973774, 16.84110333187473], - [8.86054922630853, 16.708549904608123], - [8.8818425104034, 16.57736370665313], - [8.916408329433118, 16.448803365550514], - [8.963890008337529, 16.32409989865731], - [9.023809951462665, 16.204445160433327], - [9.095574939782063, 16.090980734512833], - [9.178482372830471, 15.984787361679347], - [9.27172738273566, 15.886874988704175], - [9.374410746275132, 15.79817351671959], - [9.485547519713867, 15.719524321363023], - [9.604076320125786, 15.65167261032016], - [9.728869175858854, 15.595260677076443], - [9.858741867725827, 15.550822102625256], - [9.992464681395326, 15.518776949553867], - [10.128773490365873, 15.49942798532747], - [10.266381087897429, 15.492957963729829], - [10.403988685428985, 15.49942798532747], - [10.540297494399534, 15.518776949553867], - [10.674020308069032, 15.550822102625256], - [10.803892999936004, 15.595260677076443], - [10.928685855669073, 15.65167261032016], - [11.047214656080993, 15.719524321363023], - [11.158351429519726, 15.79817351671959], - [11.261034793059197, 15.886874988704175], - [11.354279802964387, 15.984787361679347], - [11.437187236012795, 16.090980734512833], - [11.508952224332194, 16.204445160433327], - [11.56887216745733, 16.32409989865731], - [11.616353846361742, 16.448803365550514], - [11.65091966539146, 16.57736370665313], - [11.672212949486328, 16.708549904608123], - [11.68000222605712, 16.84110333187473], - [11.674184423681723, 16.973749651097236], - [11.654786923721971, 17.10521096018074], - [11.621968406391844, 17.23421807359045], - [11.576018440067731, 17.359522826272265], - [11.517355772021233, 17.479910282070346], - [11.4465252905002, 17.594210724810743], - [11.364193642303901, 17.701311307580816], - [11.271143506676976, 17.800167234433463], - [11.168266545310468, 17.889812349055255], - [11.056555069142757, 17.969369007114743], - [10.937092484984092, 18.03805711326907], - [10.811042608074422, 18.095202210312237], - [10.679637949725281, 18.14024251677739], - [10.544167111304644, 18.172734820447474], - [10.40596143607577, 18.192359148568762], - [10.266381087897429, 18.198922150882048], - ], - ], - }, - }, - { - type: "Feature", - properties: { - cost: 4, - abatement: 4, - }, - geometry: { - coordinates: [ - [ - [-9.690278800554552, 26.97972035213317], - [-9.690278800554552, 24.396370522145148], - [-3.8527118185513416, 24.396370522145148], - [-3.8527118185513416, 26.97972035213317], - [-9.690278800554552, 26.97972035213317], - ], - ], - type: "Polygon", - }, - }, - { - type: "Feature", - properties: { - cost: 5, - abatement: 5, - }, - geometry: { - coordinates: [ - [ - [-7.541068855664491, 41.43353951220527], - [-7.541068855664491, 38.528044523038034], - [-1.643841285199045, 38.528044523038034], - [-1.643841285199045, 41.43353951220527], - [-7.541068855664491, 41.43353951220527], - ], - ], - type: "Polygon", - }, + query: { + ...(country && { countryCode: country }), }, - { - type: "Feature", - properties: { - cost: 6, - abatement: 6, - }, - geometry: { - coordinates: [ - [ - [0.7726470601811286, 48.46965178808685], - [0.7726470601811286, 45.718470124797705], - [6.157258642562226, 45.718470124797705], - [6.157258642562226, 48.46965178808685], - [0.7726470601811286, 48.46965178808685], - ], - ], - type: "Polygon", - }, - }, - { - type: "Feature", - properties: { - cost: 7, - abatement: 7, - }, - geometry: { - coordinates: [ - [ - [18.520997774407675, 51.43117929808446], - [18.520997774407675, 47.08565668322254], - [25.385706207893833, 47.08565668322254], - [25.385706207893833, 51.43117929808446], - [18.520997774407675, 51.43117929808446], - ], - ], - type: "Polygon", - }, - }, - { - type: "Feature", - properties: { - cost: 8, - abatement: 8, - }, - geometry: { - coordinates: [ - [ - [35.91663289081157, 51.36524697256908], - [35.91663289081157, 45.31005648052533], - [45.92258788209065, 45.31005648052533], - [45.92258788209065, 51.36524697256908], - [35.91663289081157, 51.36524697256908], - ], - ], - type: "Polygon", - }, - }, - { - type: "Feature", - properties: { - cost: 9, - abatement: 9, - }, - geometry: { - coordinates: [ - [ - [-6.867226241868707, 54.62702931692684], - [-6.867226241868707, 51.419015757241226], - [2.883293358353882, 51.419015757241226], - [2.883293358353882, 54.62702931692684], - [-6.867226241868707, 54.62702931692684], - ], - ], - type: "Polygon", - }, - }, - { - type: "Feature", - properties: { - cost: 10, - abatement: 0, - }, - geometry: { - coordinates: [ - [ - [38.47505991918618, 37.280028551805856], - [38.47505991918618, 29.10048451439505], - [50.31049906445517, 29.10048451439505], - [50.31049906445517, 37.280028551805856], - [38.47505991918618, 37.280028551805856], - ], - ], - type: "Polygon", - }, - }, - ], - }; + }, + { + select: (d) => d.body, + queryKey, + }, + ); - const costAbatementSource = { - id: "cost-abatement-source", - type: "geojson", - data: _data, - }; + if (isSuccess && data) { + const costAbatementSource = { + id: "cost-abatement-source", + type: "geojson", + data, + }; - const maxCost = d3.max( - costAbatementSource.data.features, - (d) => d.properties.cost, - ); - const maxAbatement = d3.max( - costAbatementSource.data.features, - (d) => d.properties.abatement, - ); + const maxCost = d3.max(data.features, (d) => d.properties.cost); + const maxAbatement = d3.max( + data.features, + (d) => d.properties.abatementPotential, + ); - const COLOR_NUMBER = 2; - const colors = generateColorRamp(COLOR_NUMBER); + const COLOR_NUMBER = 2; + const colors = generateColorRamp(COLOR_NUMBER); - const costAbatementLayer: FillLayerSpecification = { - id: "cost-abatement-layer", - type: "fill", - source: costAbatementSource.id, - paint: { - "fill-color": [ - "match", - [ - "concat", + const costAbatementLayer: FillLayerSpecification = { + id: "cost-abatement-layer", + type: "fill", + source: costAbatementSource.id, + paint: { + "fill-color": [ + "match", [ - "ceil", + "concat", [ - "/", - ["*", ["/", ["get", "cost"], maxCost], 100], - 100 / COLOR_NUMBER, + "ceil", + [ + "/", + ["*", ["/", ["get", "cost"], maxCost], 100], + 100 / COLOR_NUMBER, + ], ], - ], - [ - "ceil", [ - "/", - ["*", ["/", ["get", "abatement"], maxAbatement], 100], - 100 / COLOR_NUMBER, + "ceil", + [ + "/", + ["*", ["/", ["get", "abatementPotential"], maxAbatement], 100], + 100 / COLOR_NUMBER, + ], ], ], + ...colors, + "#FFF", ], - ...colors, - "#FFF", - ], - "fill-opacity": 1, - }, - }; + "fill-opacity": 1, + }, + }; - return ( - <> - - - - ); + return ( + <> + + + + ); + } + + return null; } diff --git a/client/src/lib/query-keys.ts b/client/src/lib/query-keys.ts index 96daeb3..4a81f41 100644 --- a/client/src/lib/query-keys.ts +++ b/client/src/lib/query-keys.ts @@ -11,4 +11,9 @@ export const authKeys = createQueryKeys("auth", { export const userKeys = createQueryKeys("user", { me: (token: string) => ["me", token], }); -export const queryKeys = mergeQueryKeys(authKeys, userKeys); + +export const geometriesKeys = createQueryKeys("geometries", { + all: null, + country: (country: string) => ["country", country], +}); +export const queryKeys = mergeQueryKeys(authKeys, userKeys, geometriesKeys); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2489dd4..2418cd7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,9 +6,33 @@ settings: catalogs: default: + '@types/geojson': + specifier: 7946.0.14 + version: 7946.0.14 '@types/node': specifier: 20.14.2 version: 20.14.2 + bcrypt: + specifier: 5.1.1 + version: 5.1.1 + class-transformer: + specifier: 0.5.1 + version: 0.5.1 + class-validator: + specifier: 0.14.1 + version: 0.14.1 + nestjs-base-service: + specifier: 0.11.1 + version: 0.11.1 + pg: + specifier: 8.12.0 + version: 8.12.0 + reflect-metadata: + specifier: ^0.2.0 + version: 0.2.2 + typeorm: + specifier: 0.3.20 + version: 0.3.20 typescript: specifier: 5.4.5 version: 5.4.5 @@ -174,6 +198,9 @@ importers: '@types/express': specifier: ^4.17.17 version: 4.17.21 + '@types/geojson': + specifier: 'catalog:' + version: 7946.0.14 '@types/jest': specifier: ^29.5.12 version: 29.5.12 @@ -350,7 +377,7 @@ importers: specifier: 7.4.3 version: 7.4.3 '@types/geojson': - specifier: 7946.0.14 + specifier: 'catalog:' version: 7946.0.14 '@types/mapbox-gl': specifier: 3.4.0 @@ -453,6 +480,9 @@ importers: '@types/bcrypt': specifier: ^5.0.2 version: 5.0.2 + '@types/geojson': + specifier: 'catalog:' + version: 7946.0.14 '@types/jsonwebtoken': specifier: 9.0.7 version: 9.0.7 @@ -9419,7 +9449,7 @@ snapshots: '@eslint/eslintrc@2.1.4': dependencies: ajv: 6.12.6 - debug: 4.3.6 + debug: 4.3.6(supports-color@5.5.0) espree: 9.6.1 globals: 13.24.0 ignore: 5.3.2 @@ -9472,7 +9502,7 @@ snapshots: '@humanwhocodes/config-array@0.11.14': dependencies: '@humanwhocodes/object-schema': 2.0.3 - debug: 4.3.6 + debug: 4.3.6(supports-color@5.5.0) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -11413,7 +11443,7 @@ snapshots: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.4.5) '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.3.6 + debug: 4.3.6(supports-color@5.5.0) eslint: 8.57.0 optionalDependencies: typescript: 5.4.5 @@ -11429,7 +11459,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.4.5) '@typescript-eslint/utils': 7.18.0(eslint@8.57.0)(typescript@5.4.5) - debug: 4.3.6 + debug: 4.3.6(supports-color@5.5.0) eslint: 8.57.0 ts-api-utils: 1.3.0(typescript@5.4.5) optionalDependencies: @@ -11443,7 +11473,7 @@ snapshots: dependencies: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.3.6 + debug: 4.3.6(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.5 @@ -12542,10 +12572,6 @@ snapshots: dependencies: ms: 2.1.3 - debug@4.3.6: - dependencies: - ms: 2.1.2 - debug@4.3.6(supports-color@5.5.0): dependencies: ms: 2.1.2 @@ -12840,7 +12866,7 @@ snapshots: eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.30.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0) + eslint-plugin-import: 2.30.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.10.0(eslint@8.57.0) eslint-plugin-react: 7.35.2(eslint@8.57.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0) @@ -12866,7 +12892,7 @@ snapshots: eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0): dependencies: '@nolyfill/is-core-module': 1.0.39 - debug: 4.3.6 + debug: 4.3.6(supports-color@5.5.0) enhanced-resolve: 5.17.1 eslint: 8.57.0 eslint-module-utils: 2.9.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) @@ -12875,7 +12901,7 @@ snapshots: is-bun-module: 1.1.0 is-glob: 4.0.3 optionalDependencies: - eslint-plugin-import: 2.30.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0) + eslint-plugin-import: 2.30.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) transitivePeerDependencies: - '@typescript-eslint/parser' - eslint-import-resolver-node @@ -12893,7 +12919,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0): + eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -13001,7 +13027,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.6 + debug: 4.3.6(supports-color@5.5.0) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f91ab53..53210b1 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -14,6 +14,7 @@ catalog: class-transformer: "0.5.1" bcrypt: "5.1.1" "@types/node": 20.14.2 + "@types/geojson": 7946.0.14 typescript: 5.4.5 reflect-metadata: "^0.2.0" class-validator: "0.14.1" diff --git a/shared/contracts/map.contract.ts b/shared/contracts/map.contract.ts index bb5a222..41546d9 100644 --- a/shared/contracts/map.contract.ts +++ b/shared/contracts/map.contract.ts @@ -1,6 +1,7 @@ import { initContract } from "@ts-rest/core"; import { z } from "zod"; -import { FeatureCollection } from "typeorm"; +import { FeatureCollection, Geometry } from "geojson"; +import { ProjectGeoProperties } from "@shared/schemas/geometries/projects"; const contract = initContract(); export const mapContract = contract.router({ @@ -8,7 +9,7 @@ export const mapContract = contract.router({ method: "GET", path: "/map/geo-features", responses: { - 200: contract.type(), + 200: contract.type>(), }, query: z.object({ countryCode: z.string().length(3).optional() }), }, diff --git a/shared/package.json b/shared/package.json index 76d22f9..9b58f24 100644 --- a/shared/package.json +++ b/shared/package.json @@ -4,6 +4,7 @@ "devDependencies": { "@nestjs/mapped-types": "^2.0.5", "@types/bcrypt": "^5.0.2", + "@types/geojson": "catalog:", "@types/jsonwebtoken": "9.0.7", "@types/lodash": "4.17.10", "@types/node": "catalog:", diff --git a/shared/schemas/geometries/projects.ts b/shared/schemas/geometries/projects.ts new file mode 100644 index 0000000..faf35a3 --- /dev/null +++ b/shared/schemas/geometries/projects.ts @@ -0,0 +1,10 @@ +import { z } from "zod"; + +export const ProjectGeoPropertiesSchema = z.object({ + abatementPotential: z.number(), + cost: z.number(), + country: z.string(), +}); + +export type ProjectGeoProperties = z.infer; +