From a74047fd099d9c25ae6db33cfae9040022f3ea3e Mon Sep 17 00:00:00 2001 From: taDuc Date: Sat, 2 May 2026 02:48:17 +0700 Subject: [PATCH] add editor --- api.ts | 8 +- package-lock.json | 508 ++++- package.json | 2 + public/point.png | Bin 0 -> 15508 bytes src/app/editor/[id]/featureCommands.ts | 114 ++ src/app/editor/[id]/page.tsx | 794 ++++++++ src/app/editor/page.tsx | 7 + src/app/user/projects/[id]/page.tsx | 8 +- src/app/user/projects/page.tsx | 12 +- src/app/user/submissions/[id]/page.tsx | 331 ++++ src/app/user/submissions/page.tsx | 326 ++++ src/interface/project.ts | 3 +- src/interface/submission.ts | 50 + src/service/submissionService.ts | 37 + src/uhm/api/auth.ts | 33 + src/uhm/api/config.ts | 23 + src/uhm/api/entities.ts | 32 + src/uhm/api/geometries.ts | 92 + src/uhm/api/http.ts | 154 ++ src/uhm/api/sections.ts | 187 ++ src/uhm/api/tiles.ts | 20 + src/uhm/components/AuthPanel.tsx | 9 + src/uhm/components/BackgroundLayersPanel.tsx | 95 + src/uhm/components/CommitTreePopup.tsx | 349 ++++ src/uhm/components/Editor.tsx | 491 +++++ src/uhm/components/Map.tsx | 1648 +++++++++++++++++ src/uhm/components/SelectedGeometryPanel.tsx | 493 +++++ src/uhm/components/TimelineBar.tsx | 140 ++ src/uhm/lib/backgroundLayers.ts | 28 + .../background/backgroundVisibilityStorage.ts | 58 + src/uhm/lib/editor/draft/draftDiff.ts | 55 + src/uhm/lib/editor/draft/editorTypes.ts | 15 + src/uhm/lib/editor/draft/useDraftState.ts | 31 + src/uhm/lib/editor/draft/useUndoStack.ts | 81 + src/uhm/lib/editor/entity/entityBinding.ts | 109 ++ .../lib/editor/geometry/geometryMetadata.ts | 50 + .../lib/editor/section/useSectionCommands.ts | 257 +++ src/uhm/lib/editor/session/sessionTypes.ts | 44 + .../session/useBackgroundSessionState.ts | 21 + .../editor/session/useEntitySessionState.ts | 80 + .../editor/session/useSectionSessionState.ts | 85 + .../lib/editor/session/useTimelineState.ts | 42 + src/uhm/lib/editor/snapshot/editorSnapshot.ts | 254 +++ src/uhm/lib/engine/circleEngine.ts | 247 +++ src/uhm/lib/engine/drawingEngine.ts | 127 ++ src/uhm/lib/engine/editingEngine.ts | 226 +++ src/uhm/lib/engine/engineTypes.ts | 4 + src/uhm/lib/engine/lineEngine.ts | 140 ++ src/uhm/lib/engine/pathEngine.ts | 142 ++ src/uhm/lib/engine/pointEngine.ts | 45 + src/uhm/lib/engine/selectingEngine.ts | 258 +++ src/uhm/lib/entityTypeOptions.ts | 122 ++ src/uhm/lib/geo/constants.ts | 14 + src/uhm/lib/map/constants.ts | 13 + src/uhm/lib/map/style.ts | 76 + src/uhm/lib/timeline.ts | 25 + src/uhm/lib/useEditorSessionState.ts | 51 + src/uhm/lib/useEditorState.ts | 186 ++ src/uhm/types/api.ts | 18 + src/uhm/types/entities.ts | 31 + src/uhm/types/geo.ts | 70 + src/uhm/types/sections.ts | 87 + 62 files changed, 9049 insertions(+), 9 deletions(-) create mode 100644 public/point.png create mode 100644 src/app/editor/[id]/featureCommands.ts create mode 100644 src/app/editor/[id]/page.tsx create mode 100644 src/app/editor/page.tsx create mode 100644 src/app/user/submissions/[id]/page.tsx create mode 100644 src/app/user/submissions/page.tsx create mode 100644 src/interface/submission.ts create mode 100644 src/service/submissionService.ts create mode 100644 src/uhm/api/auth.ts create mode 100644 src/uhm/api/config.ts create mode 100644 src/uhm/api/entities.ts create mode 100644 src/uhm/api/geometries.ts create mode 100644 src/uhm/api/http.ts create mode 100644 src/uhm/api/sections.ts create mode 100644 src/uhm/api/tiles.ts create mode 100644 src/uhm/components/AuthPanel.tsx create mode 100644 src/uhm/components/BackgroundLayersPanel.tsx create mode 100644 src/uhm/components/CommitTreePopup.tsx create mode 100644 src/uhm/components/Editor.tsx create mode 100644 src/uhm/components/Map.tsx create mode 100644 src/uhm/components/SelectedGeometryPanel.tsx create mode 100644 src/uhm/components/TimelineBar.tsx create mode 100644 src/uhm/lib/backgroundLayers.ts create mode 100644 src/uhm/lib/editor/background/backgroundVisibilityStorage.ts create mode 100644 src/uhm/lib/editor/draft/draftDiff.ts create mode 100644 src/uhm/lib/editor/draft/editorTypes.ts create mode 100644 src/uhm/lib/editor/draft/useDraftState.ts create mode 100644 src/uhm/lib/editor/draft/useUndoStack.ts create mode 100644 src/uhm/lib/editor/entity/entityBinding.ts create mode 100644 src/uhm/lib/editor/geometry/geometryMetadata.ts create mode 100644 src/uhm/lib/editor/section/useSectionCommands.ts create mode 100644 src/uhm/lib/editor/session/sessionTypes.ts create mode 100644 src/uhm/lib/editor/session/useBackgroundSessionState.ts create mode 100644 src/uhm/lib/editor/session/useEntitySessionState.ts create mode 100644 src/uhm/lib/editor/session/useSectionSessionState.ts create mode 100644 src/uhm/lib/editor/session/useTimelineState.ts create mode 100644 src/uhm/lib/editor/snapshot/editorSnapshot.ts create mode 100644 src/uhm/lib/engine/circleEngine.ts create mode 100644 src/uhm/lib/engine/drawingEngine.ts create mode 100644 src/uhm/lib/engine/editingEngine.ts create mode 100644 src/uhm/lib/engine/engineTypes.ts create mode 100644 src/uhm/lib/engine/lineEngine.ts create mode 100644 src/uhm/lib/engine/pathEngine.ts create mode 100644 src/uhm/lib/engine/pointEngine.ts create mode 100644 src/uhm/lib/engine/selectingEngine.ts create mode 100644 src/uhm/lib/entityTypeOptions.ts create mode 100644 src/uhm/lib/geo/constants.ts create mode 100644 src/uhm/lib/map/constants.ts create mode 100644 src/uhm/lib/map/style.ts create mode 100644 src/uhm/lib/timeline.ts create mode 100644 src/uhm/lib/useEditorSessionState.ts create mode 100644 src/uhm/lib/useEditorState.ts create mode 100644 src/uhm/types/api.ts create mode 100644 src/uhm/types/entities.ts create mode 100644 src/uhm/types/geo.ts create mode 100644 src/uhm/types/sections.ts diff --git a/api.ts b/api.ts index d3069b2..ee28e72 100644 --- a/api.ts +++ b/api.ts @@ -58,4 +58,10 @@ export const API = { GET_COMMITS: (id: number | string) => `${API_URL_ROOT}/projects/${id}/commits`, RESTORE_COMMIT: (id: number | string) => `${API_URL_ROOT}/projects/${id}/commits/restore`, }, -} \ No newline at end of file + Submission: { + SEARCH: `${API_URL_ROOT}/submissions`, + GET_BY_ID: (id: number | string) => `${API_URL_ROOT}/submissions/${id}`, + UPDATE_STATUS: (id: number | string) => `${API_URL_ROOT}/submissions/${id}/status`, + DELETE: (id: number | string) => `${API_URL_ROOT}/submissions/${id}`, + }, +} diff --git a/package-lock.json b/package-lock.json index a135486..ff183d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,9 +23,11 @@ "autoprefixer": "^10.4.22", "axios": "^1.14.0", "flatpickr": "^4.6.13", + "maplibre-gl": "^5.20.2", "next": "^16.1.6", "react": "^19.2.0", "react-apexcharts": "^1.8.0", + "react-d3-tree": "^3.6.6", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dom": "^19.2.0", @@ -95,6 +97,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1765,6 +1768,24 @@ "node": ">=6.9.0" } }, + "node_modules/@bkrem/react-transition-group": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@bkrem/react-transition-group/-/react-transition-group-1.3.5.tgz", + "integrity": "sha512-lbBYhC42sxAeFEopxzd9oWdkkV0zirO5E9WyeOBxOrpXsf7m30Aj8vnbayZxFOwD9pvUQ2Pheb1gO79s0Qap3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "chain-function": "^1.0.0", + "dom-helpers": "^3.3.1", + "loose-envify": "^1.3.1", + "prop-types": "^15.5.6", + "react-lifecycles-compat": "^3.0.4", + "warning": "^3.0.0" + }, + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@emnapi/core": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", @@ -1945,6 +1966,7 @@ "resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.20.tgz", "integrity": "sha512-1cukXLlePFiJ8YKXn/4tMKsy0etxYLCkXk8nUCFi11nRONF2Ba2CD5b21/ovtOO2tL6afTJfwmc1ed3HG7eB1g==", "license": "MIT", + "peer": true, "dependencies": { "preact": "~10.12.1" } @@ -2562,6 +2584,111 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mapbox/jsonlint-lines-primitives": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", + "integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@mapbox/point-geometry": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-1.1.0.tgz", + "integrity": "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==", + "license": "ISC" + }, + "node_modules/@mapbox/tiny-sdf": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.1.0.tgz", + "integrity": "sha512-uFJhNh36BR4OCuWIEiWaEix9CA2WzT6CAIcqVjWYpnx8+QDtS+oC4QehRrx5cX4mgWs37MmKnwUejeHxVymzNg==", + "license": "BSD-2-Clause" + }, + "node_modules/@mapbox/unitbezier": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", + "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==", + "license": "BSD-2-Clause" + }, + "node_modules/@mapbox/vector-tile": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-2.0.4.tgz", + "integrity": "sha512-AkOLcbgGTdXScosBWwmmD7cDlvOjkg/DetGva26pIRiZPdeJYjYKarIlb4uxVzi6bwHO6EWH82eZ5Nuv4T5DUg==", + "license": "BSD-3-Clause", + "dependencies": { + "@mapbox/point-geometry": "~1.1.0", + "@types/geojson": "^7946.0.16", + "pbf": "^4.0.1" + } + }, + "node_modules/@mapbox/whoots-js": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz", + "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==", + "license": "ISC", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@maplibre/geojson-vt": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@maplibre/geojson-vt/-/geojson-vt-6.1.0.tgz", + "integrity": "sha512-2eIY4gZxeKIVOZVNkAMb+5NgXhgsMQpOveTQAvnp53LYqHGJZDidk7Ew0Tged9PThidpbS+NFTh0g4zivhPDzQ==", + "license": "ISC", + "dependencies": { + "kdbush": "^4.0.2" + } + }, + "node_modules/@maplibre/maplibre-gl-style-spec": { + "version": "24.8.1", + "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-24.8.1.tgz", + "integrity": "sha512-zxa92qF96ZNojLxeAjnaRpjVCy+swoUNJvDhtpC90k7u5F0TMr4GmvNqMKvYrMoPB8d7gRSXbMG1hBbmgESIsw==", + "license": "ISC", + "dependencies": { + "@mapbox/jsonlint-lines-primitives": "~2.0.2", + "@mapbox/unitbezier": "^0.0.1", + "json-stringify-pretty-compact": "^4.0.0", + "minimist": "^1.2.8", + "quickselect": "^3.0.0", + "rw": "^1.3.3", + "tinyqueue": "^3.0.0" + }, + "bin": { + "gl-style-format": "dist/gl-style-format.mjs", + "gl-style-migrate": "dist/gl-style-migrate.mjs", + "gl-style-validate": "dist/gl-style-validate.mjs" + } + }, + "node_modules/@maplibre/mlt": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@maplibre/mlt/-/mlt-1.1.9.tgz", + "integrity": "sha512-g/tD8EYJB97udq33ipuJ9a4Q7fcbZnTEnUrgnEc/tLMmEL+zaCbR+X5fkDBO2dgpaAMsLH179qE3UXg2N0Nc/g==", + "license": "(MIT OR Apache-2.0)", + "dependencies": { + "@mapbox/point-geometry": "^1.1.0" + } + }, + "node_modules/@maplibre/vt-pbf": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@maplibre/vt-pbf/-/vt-pbf-4.3.0.tgz", + "integrity": "sha512-jIvp8F5hQCcreqOOpEt42TJMUlsrEcpf/kI1T2v85YrQRV6PPXUcEXUg5karKtH6oh47XJZ4kHu56pUkOuqA7w==", + "license": "MIT", + "dependencies": { + "@mapbox/point-geometry": "^1.1.0", + "@mapbox/vector-tile": "^2.0.4", + "@maplibre/geojson-vt": "^5.0.4", + "@types/geojson": "^7946.0.16", + "@types/supercluster": "^7.1.3", + "pbf": "^4.0.1", + "supercluster": "^8.0.1" + } + }, + "node_modules/@maplibre/vt-pbf/node_modules/@maplibre/geojson-vt": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@maplibre/geojson-vt/-/geojson-vt-5.0.4.tgz", + "integrity": "sha512-KGg9sma45S+stfH9vPCJk1J0lSDLWZgCT9Y8u8qWZJyjFlP8MNP1WGTxIMYJZjDvVT3PDn05kN1C95Sut1HpgQ==", + "license": "ISC" + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -2894,6 +3021,7 @@ "resolved": "https://registry.npmjs.org/@svgdotjs/svg.js/-/svg.js-3.2.5.tgz", "integrity": "sha512-/VNHWYhNu+BS7ktbYoVGrCmsXDh+chFMaONMwGNdIBcFHrWqk2jY8fNyr3DLdtQUIalvkPfM554ZSFa3dm3nxQ==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/Fuzzyma" @@ -2917,6 +3045,7 @@ "resolved": "https://registry.npmjs.org/@svgdotjs/svg.select.js/-/svg.select.js-4.0.3.tgz", "integrity": "sha512-qkMgso1sd2hXKd1FZ1weO7ANq12sNmQJeGDjs46QwDVsxSRcHmvWKL2NDF7Yimpwf3sl5esOLkPqtV2bQ3v/Jg==", "license": "MIT", + "peer": true, "engines": { "node": ">= 14.18" }, @@ -3093,6 +3222,7 @@ "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -3482,6 +3612,12 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/d3-hierarchy": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-1.1.11.tgz", + "integrity": "sha512-lnQiU7jV+Gyk9oQYk0GGYccuexmQPTp08E0+4BidgFdiJivjEvf+esPSdZqCZ2C7UwTWejWpqetVaU8A+eX3FA==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -3489,6 +3625,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -3509,6 +3651,7 @@ "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -3519,6 +3662,7 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -3529,6 +3673,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -3543,6 +3688,15 @@ "@types/react": "*" } }, + "node_modules/@types/supercluster": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz", + "integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/use-sync-external-store": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", @@ -3594,6 +3748,7 @@ "integrity": "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/types": "8.58.0", @@ -4125,6 +4280,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4180,6 +4336,7 @@ "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-4.7.0.tgz", "integrity": "sha512-iZSrrBGvVlL+nt2B1NpqfDuBZ9jX61X9I2+XV0hlYXHtTwhwLTHDKGXjNXAgFBDLuvSYCB/rq2nPWVPRv2DrGA==", "license": "MIT", + "peer": true, "dependencies": { "@svgdotjs/svg.draggable.js": "^3.0.4", "@svgdotjs/svg.filter.js": "^3.0.8", @@ -4592,6 +4749,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -4698,6 +4856,12 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chain-function": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/chain-function/-/chain-function-1.0.1.tgz", + "integrity": "sha512-SxltgMwL9uCko5/ZCLiyG2B7R9fY4pDZUw7hJ4MhirdjBLosoDqkWABi3XMucddHdLiFJMb7PD2MZifZriuMTg==", + "license": "MIT" + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -4727,6 +4891,15 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -4926,6 +5099,133 @@ "devOptional": true, "license": "MIT" }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz", + "integrity": "sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -5067,6 +5367,15 @@ "node": ">=0.4.0" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -5109,6 +5418,15 @@ "node": ">=0.10.0" } }, + "node_modules/dom-helpers": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz", + "integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.1.2" + } + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -5193,6 +5511,12 @@ "node": ">= 0.4" } }, + "node_modules/earcut": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz", + "integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==", + "license": "ISC" + }, "node_modules/electron-to-chromium": { "version": "1.5.329", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.329.tgz", @@ -5444,6 +5768,7 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5629,6 +5954,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -6215,6 +6541,12 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/gl-matrix": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz", + "integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==", + "license": "MIT" + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -6922,7 +7254,8 @@ "version": "3.7.1", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/js-tokens": { "version": "4.0.0", @@ -6984,6 +7317,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stringify-pretty-compact": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz", + "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==", + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -7013,6 +7352,12 @@ "node": ">=4.0" } }, + "node_modules/kdbush": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz", + "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==", + "license": "ISC" + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -7403,6 +7748,40 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/maplibre-gl": { + "version": "5.24.0", + "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.24.0.tgz", + "integrity": "sha512-ALyFxgtd5R+65UqZ/++lOqwWcC0SNho9c27fYSyLmG7AfnAul2o46F05aDJGPbFU57wos9dgcIySHs0Xe6ia3A==", + "license": "BSD-3-Clause", + "dependencies": { + "@mapbox/jsonlint-lines-primitives": "^2.0.2", + "@mapbox/point-geometry": "^1.1.0", + "@mapbox/tiny-sdf": "^2.1.0", + "@mapbox/unitbezier": "^0.0.1", + "@mapbox/vector-tile": "^2.0.4", + "@mapbox/whoots-js": "^3.1.0", + "@maplibre/geojson-vt": "^6.1.0", + "@maplibre/maplibre-gl-style-spec": "^24.8.1", + "@maplibre/mlt": "^1.1.8", + "@maplibre/vt-pbf": "^4.3.0", + "@types/geojson": "^7946.0.16", + "earcut": "^3.0.2", + "gl-matrix": "^3.4.4", + "kdbush": "^4.0.2", + "murmurhash-js": "^1.0.0", + "pbf": "^4.0.1", + "potpack": "^2.1.0", + "quickselect": "^3.0.0", + "tinyqueue": "^3.0.0" + }, + "engines": { + "node": ">=16.14.0", + "npm": ">=8.1.0" + }, + "funding": { + "url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -7490,7 +7869,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7503,6 +7881,12 @@ "dev": true, "license": "MIT" }, + "node_modules/murmurhash-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz", + "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==", + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -7939,6 +8323,18 @@ "node": ">=8" } }, + "node_modules/pbf": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.1.tgz", + "integrity": "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==", + "license": "BSD-3-Clause", + "dependencies": { + "resolve-protobuf-schema": "^2.1.0" + }, + "bin": { + "pbf": "bin/pbf" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -7987,6 +8383,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -8002,6 +8399,12 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, + "node_modules/potpack": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.1.0.tgz", + "integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==", + "license": "ISC" + }, "node_modules/preact": { "version": "10.12.1", "resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz", @@ -8033,6 +8436,12 @@ "react-is": "^16.13.1" } }, + "node_modules/protocol-buffers-schema": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.1.tgz", + "integrity": "sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==", + "license": "MIT" + }, "node_modules/proxy-from-env": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", @@ -8073,6 +8482,12 @@ ], "license": "MIT" }, + "node_modules/quickselect": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", + "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", + "license": "ISC" + }, "node_modules/quill": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/quill/-/quill-2.0.3.tgz", @@ -8093,6 +8508,7 @@ "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-5.1.0.tgz", "integrity": "sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA==", "license": "MIT", + "peer": true, "dependencies": { "fast-diff": "^1.3.0", "lodash.clonedeep": "^4.5.0", @@ -8107,6 +8523,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -8124,6 +8541,27 @@ "react": ">=16.8.0" } }, + "node_modules/react-d3-tree": { + "version": "3.6.6", + "resolved": "https://registry.npmjs.org/react-d3-tree/-/react-d3-tree-3.6.6.tgz", + "integrity": "sha512-E9ByUdeqvlxLlF9BSL7KWQH3ikYHtHO+g1rAPcVgj6mu92tjRUCan2AWxoD4eTSzzAATf8BZtf+CXGSoSd6ioQ==", + "license": "MIT", + "dependencies": { + "@bkrem/react-transition-group": "^1.3.5", + "@types/d3-hierarchy": "^1.1.8", + "clone": "^2.1.1", + "d3-hierarchy": "^1.1.9", + "d3-selection": "^3.0.0", + "d3-shape": "^1.3.7", + "d3-zoom": "^3.0.0", + "dequal": "^2.0.2", + "uuid": "^8.3.1" + }, + "peerDependencies": { + "react": "16.x || 17.x || 18.x || 19.x", + "react-dom": "16.x || 17.x || 18.x || 19.x" + } + }, "node_modules/react-dnd": { "version": "16.0.1", "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz", @@ -8168,6 +8606,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -8198,6 +8637,12 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", + "license": "MIT" + }, "node_modules/react-quill-new": { "version": "3.8.3", "resolved": "https://registry.npmjs.org/react-quill-new/-/react-quill-new-3.8.3.tgz", @@ -8218,6 +8663,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -8240,7 +8686,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -8400,6 +8847,15 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/resolve-protobuf-schema": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", + "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==", + "license": "MIT", + "dependencies": { + "protocol-buffers-schema": "^3.3.1" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -8435,6 +8891,12 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, "node_modules/safe-array-concat": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", @@ -8932,6 +9394,15 @@ } } }, + "node_modules/supercluster": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz", + "integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==", + "license": "ISC", + "dependencies": { + "kdbush": "^4.0.2" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -9034,7 +9505,8 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tapable": { "version": "2.3.2", @@ -9090,6 +9562,7 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -9097,6 +9570,12 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinyqueue": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", + "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", + "license": "ISC" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -9252,6 +9731,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -9438,6 +9918,25 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz", + "integrity": "sha512-jMBt6pUrKn5I+OGgtQ4YZLdhIeJmObddh6CsibPxyQ5yPZm1XExSyzC1LCNX7BzhxWgiHmizBWJTHJIjMjTQYQ==", + "license": "BSD-3-Clause", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -9605,6 +10104,7 @@ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 1903a1c..950118c 100644 --- a/package.json +++ b/package.json @@ -24,9 +24,11 @@ "autoprefixer": "^10.4.22", "axios": "^1.14.0", "flatpickr": "^4.6.13", + "maplibre-gl": "^5.20.2", "next": "^16.1.6", "react": "^19.2.0", "react-apexcharts": "^1.8.0", + "react-d3-tree": "^3.6.6", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dom": "^19.2.0", diff --git a/public/point.png b/public/point.png new file mode 100644 index 0000000000000000000000000000000000000000..99b5c6e486c965d03d94d2f7fccb8bf686a021a4 GIT binary patch literal 15508 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 literal 0 HcmV?d00001 diff --git a/src/app/editor/[id]/featureCommands.ts b/src/app/editor/[id]/featureCommands.ts new file mode 100644 index 0000000..92c7829 --- /dev/null +++ b/src/app/editor/[id]/featureCommands.ts @@ -0,0 +1,114 @@ +"use client"; + +import { useCallback } from "react"; +import type { Dispatch, SetStateAction } from "react"; +import type { Entity } from "@/uhm/types/entities"; +import type { Feature, FeatureProperties } from "@/uhm/types/geo"; +import { ApiError } from "@/uhm/api/http"; +import { buildFeatureEntityPatch } from "@/uhm/lib/editor/entity/entityBinding"; +import { buildGeometryMetadataPatch } from "@/uhm/lib/editor/geometry/geometryMetadata"; +import { uniqueEntityIds } from "@/uhm/lib/editor/snapshot/editorSnapshot"; +import type { GeometryMetaFormState } from "@/uhm/lib/editor/session/sessionTypes"; + +type EditorDraftApi = { + patchFeatureProperties: (id: FeatureProperties["id"], patch: Partial) => void; +}; + +type Options = { + editor: EditorDraftApi; + selectedFeature: Feature | null; + geometryMetaForm: GeometryMetaFormState; + setGeometryMetaForm: Dispatch>; + selectedGeometryEntityIds: string[]; + setSelectedGeometryEntityIds: Dispatch>; + entities: Entity[]; + setIsEntitySubmitting: Dispatch>; + setEntityFormStatus: Dispatch>; +}; + +export function useFeatureCommands(options: Options) { + const { + editor, + selectedFeature, + geometryMetaForm, + setGeometryMetaForm, + selectedGeometryEntityIds, + setSelectedGeometryEntityIds, + entities, + setIsEntitySubmitting, + setEntityFormStatus, + } = options; + + const applyGeometryMetadata = useCallback(async () => { + if (!selectedFeature) { + setEntityFormStatus("Hãy chọn một geometry trước."); + return; + } + + let metadata; + try { + metadata = buildGeometryMetadataPatch(geometryMetaForm); + } catch (err) { + setEntityFormStatus(err instanceof Error ? err.message : "Thời gian không hợp lệ."); + return; + } + + setIsEntitySubmitting(true); + setEntityFormStatus(null); + try { + editor.patchFeatureProperties(selectedFeature.properties.id, metadata.patch); + setGeometryMetaForm(metadata.formState); + setEntityFormStatus("Đã cập nhật thuộc tính GEO. Commit khi sẵn sàng."); + } finally { + setIsEntitySubmitting(false); + } + }, [ + editor, + geometryMetaForm, + selectedFeature, + setEntityFormStatus, + setGeometryMetaForm, + setIsEntitySubmitting, + ]); + + const applyEntitiesToSelectedGeometry = useCallback(async () => { + if (!selectedFeature) { + setEntityFormStatus("Hãy chọn một geometry trước."); + return; + } + + const entityIds = uniqueEntityIds(selectedGeometryEntityIds); + setIsEntitySubmitting(true); + setEntityFormStatus(null); + try { + editor.patchFeatureProperties( + selectedFeature.properties.id, + buildFeatureEntityPatch(selectedFeature, entityIds, entities) + ); + setSelectedGeometryEntityIds(entityIds); + setEntityFormStatus("Đã cập nhật danh sách entity. Commit khi sẵn sàng."); + } catch (err) { + if (err instanceof ApiError) { + setEntityFormStatus(`Lưu thất bại: ${err.body}`); + } else { + setEntityFormStatus("Lưu thất bại."); + } + } finally { + setIsEntitySubmitting(false); + } + }, [ + editor, + entities, + selectedFeature, + selectedGeometryEntityIds, + setEntityFormStatus, + setIsEntitySubmitting, + setSelectedGeometryEntityIds, + ]); + + return { + applyGeometryMetadata, + applyEntitiesToSelectedGeometry, + }; +} + diff --git a/src/app/editor/[id]/page.tsx b/src/app/editor/[id]/page.tsx new file mode 100644 index 0000000..bf861d8 --- /dev/null +++ b/src/app/editor/[id]/page.tsx @@ -0,0 +1,794 @@ +"use client"; + +import { useCallback, useEffect, useMemo, useRef } from "react"; +import { useParams, useRouter } from "next/navigation"; +import Map from "@/uhm/components/Map"; +import Editor from "@/uhm/components/Editor"; +import BackgroundLayersPanel from "@/uhm/components/BackgroundLayersPanel"; +import TimelineBar from "@/uhm/components/TimelineBar"; +import SelectedGeometryPanel from "@/uhm/components/SelectedGeometryPanel"; +import { Entity, fetchEntities, searchEntitiesByName } from "@/uhm/api/entities"; +import { ApiError } from "@/uhm/api/http"; +import { fetchCurrentUser } from "@/uhm/api/auth"; +import { fetchGeometriesByBBox } from "@/uhm/api/geometries"; +import { SectionCommit } from "@/uhm/api/sections"; +import { + Feature, + useEditorState, +} from "@/uhm/lib/useEditorState"; +import { + BackgroundLayerId, + BackgroundLayerVisibility, + DEFAULT_BACKGROUND_LAYER_VISIBILITY, + HIDDEN_BACKGROUND_LAYER_VISIBILITY, +} from "@/uhm/lib/backgroundLayers"; +import { + DEFAULT_ENTITY_TYPE_ID, + ENTITY_TYPE_OPTIONS, + EntityTypeGroupId, + findEntityTypeOption, +} from "@/uhm/lib/entityTypeOptions"; +import { + EntityFormState, + PendingEntityCreate, + useEditorSessionState, +} from "@/uhm/lib/useEditorSessionState"; +import { + getDefaultTypeIdForFeature, + normalizeFeatureBindingIds, + normalizeFeatureEntityIds, + uniqueEntityIds, +} from "@/uhm/lib/editor/snapshot/editorSnapshot"; +import { + buildClientEntityId, + formatEntityNamesForDisplay, + mergeEntitiesWithPending, + mergeEntitySearchResults, +} from "@/uhm/lib/editor/entity/entityBinding"; +import { + formatBindingIdsForDisplay, +} from "@/uhm/lib/editor/geometry/geometryMetadata"; +import { + loadBackgroundLayerVisibilityFromStorage, + persistBackgroundLayerVisibility, +} from "@/uhm/lib/editor/background/backgroundVisibilityStorage"; +import { useSectionCommands } from "@/uhm/lib/editor/section/useSectionCommands"; +import { EMPTY_FEATURE_COLLECTION, WORLD_BBOX } from "@/uhm/lib/geo/constants"; +import { FIXED_TIMELINE_RANGE, clampYearToFixedRange, TIMELINE_DEBOUNCE_MS } from "@/uhm/lib/timeline"; +import { useFeatureCommands } from "./featureCommands"; + +const CURRENT_YEAR = new Date().getUTCFullYear(); +const DEFAULT_EDITOR_USER_ID = "local-editor"; + +export default function Page() { + const params = useParams(); + const router = useRouter(); + const projectId = String(params.id || ""); + const openedProjectIdRef = useRef(null); + + const { + mode, + setMode, + initialData, + setInitialData, + isSaving, + setIsSaving, + isSubmitting, + setIsSubmitting, + isOpeningSection, + setIsOpeningSection, + availableSections, + setAvailableSections, + selectedSectionId, + setSelectedSectionId, + newSectionTitle, + setNewSectionTitle, + commitTitle, + setCommitTitle, + commitNote, + setCommitNote, + editorUserIdInput, + setEditorUserIdInput, + activeSection, + setActiveSection, + sectionState, + setSectionState, + sectionCommits, + setSectionCommits, + lastSectionSnapshot, + setLastSectionSnapshot, + persistedEntities, + setPersistedEntities, + pendingEntityCreates, + setPendingEntityCreates, + createdEntities, + setCreatedEntities, + entityStatus, + setEntityStatus, + selectedFeatureId, + setSelectedFeatureId, + entityForm, + setEntityForm, + selectedGeometryEntityIds, + setSelectedGeometryEntityIds, + geometryMetaForm, + setGeometryMetaForm, + isEntitySubmitting, + setIsEntitySubmitting, + entityFormStatus, + setEntityFormStatus, + entitySearchQuery, + setEntitySearchQuery, + entitySearchResults, + setEntitySearchResults, + selectedSearchEntityId, + setSelectedSearchEntityId, + isEntitySearchLoading, + setIsEntitySearchLoading, + timelineYear, + setTimelineYear, + timelineDraftYear, + setTimelineDraftYear, + isTimelineLoading, + setIsTimelineLoading, + timelineStatus, + setTimelineStatus, + backgroundVisibility, + setBackgroundVisibility, + isBackgroundVisibilityReady, + setIsBackgroundVisibilityReady, + } = useEditorSessionState({ + emptyFeatureCollection: EMPTY_FEATURE_COLLECTION, + defaultEditorUserId: DEFAULT_EDITOR_USER_ID, + fallbackTimelineRange: FIXED_TIMELINE_RANGE, + currentYear: CURRENT_YEAR, + }); + // Counter để bỏ qua response cũ khi user đổi timeline/section liên tục. + const timelineFetchRequestRef = useRef(0); + // Counter để bỏ qua response cũ khi user gõ search entity liên tục. + const entitySearchRequestRef = useRef(0); + + const editor = useEditorState(initialData); + const editorUserId = normalizeEditorUserId(editorUserIdInput); + const entities = useMemo( + () => mergeEntitiesWithPending(persistedEntities, pendingEntityCreates), + [persistedEntities, pendingEntityCreates] + ); + const selectedFeature = + selectedFeatureId === null + ? null + : editor.draft.features.find((feature) => + String(feature.properties.id) === String(selectedFeatureId) + ) || null; + + const createdGeometries = useMemo(() => { + const rows: Array<{ + id: string | number; + geometryType: string; + semanticType?: string | null; + entityNames: string[]; + }> = []; + + for (const change of editor.changes.values()) { + if (change.action !== "create") continue; + const feature = change.feature; + const entityNames = normalizeFeatureEntityIds(feature) + .map((entityId) => entities.find((entity) => entity.id === entityId)?.name || entityId); + + rows.push({ + id: feature.properties.id, + geometryType: feature.geometry.type, + semanticType: feature.properties.type || getDefaultTypeIdForFeature(feature), + entityNames, + }); + } + + return rows; + }, [editor.changes, entities]); + + const sectionCommands = useSectionCommands({ + editor, + editorUserId, + emptyFeatureCollection: EMPTY_FEATURE_COLLECTION, + activeSection, + sectionState, + selectedSectionId, + newSectionTitle, + pendingSaveCount: editor.changeCount + pendingEntityCreates.length, + pendingEntityCreates, + lastSectionSnapshot, + commitTitle, + commitNote, + setActiveSection, + setSelectedSectionId, + setSectionState, + setLastSectionSnapshot, + setInitialData, + setSectionCommits, + setPendingEntityCreates, + setCreatedEntities, + setEntityFormStatus, + setSelectedFeatureId, + setEntityStatus, + setIsSaving, + setIsSubmitting, + setIsOpeningSection, + setAvailableSections, + setNewSectionTitle, + setCommitTitle, + setCommitNote, + }); + const { + openSectionForEditing, + commitSection, + submitCurrentSection, + restoreCommit, + } = sectionCommands; + + const openProject = useCallback(async () => { + if (!projectId) return; + try { + setIsOpeningSection(true); + setEntityStatus(null); + await openSectionForEditing(projectId); + setEntityStatus(null); + } catch (err) { + if (err instanceof ApiError) { + if (err.status === 401 || err.status === 400) { + router.replace("/signin"); + return; + } + setEntityStatus(`Mở project thất bại: ${err.body || err.message}`); + } else { + console.error("Open project failed", err); + setEntityStatus("Mở project thất bại."); + } + } finally { + setIsOpeningSection(false); + } + }, [openSectionForEditing, projectId, router, setEntityStatus, setIsOpeningSection]); + + useEffect(() => { + let disposed = false; + + async function ensureAuthenticated() { + try { + await fetchCurrentUser(); + } catch (err) { + if (disposed) return; + // Follow the same behavior as the rest of FrontEndAdmin: unauthenticated -> /signin. + router.replace("/signin"); + } + } + + ensureAuthenticated(); + return () => { + disposed = true; + }; + }, [router]); + + useEffect(() => { + if (!projectId) return; + if (openedProjectIdRef.current === projectId) return; + + openProject() + .then(() => { + openedProjectIdRef.current = projectId; + }) + .catch(() => { + // allow retry if openProject threw outside its try/catch (should be rare) + openedProjectIdRef.current = null; + }); + }, [openProject]); + + useEffect(() => { + let disposed = false; + + async function loadEntities() { + try { + const rows = await fetchEntities(); + if (disposed) return; + + setPersistedEntities(rows); + setEntityStatus(null); + } catch (err) { + if (disposed) return; + console.error("Load entities failed", err); + setEntityStatus("Không tải được danh sách entity."); + } + } + + loadEntities(); + + return () => { + disposed = true; + }; + }, [setEntityStatus, setPersistedEntities]); + + useEffect(() => { + if (!selectedFeature) { + setEntitySearchResults([]); + setSelectedSearchEntityId(null); + setIsEntitySearchLoading(false); + return; + } + + const keyword = entitySearchQuery.trim(); + if (!keyword.length) { + setEntitySearchResults([]); + setSelectedSearchEntityId(null); + setIsEntitySearchLoading(false); + return; + } + + let disposed = false; + const requestId = ++entitySearchRequestRef.current; + const timeoutId = window.setTimeout(async () => { + setIsEntitySearchLoading(true); + try { + const rows = await searchEntitiesByName(keyword, { limit: 30 }); + if (disposed || requestId !== entitySearchRequestRef.current) return; + + const pendingMatches = pendingEntityCreates + .filter((entity) => + entity.name.toLowerCase().includes(keyword.toLowerCase()) || + (entity.slug || "").toLowerCase().includes(keyword.toLowerCase()) + ) + .map((entity) => ({ + id: entity.id, + name: entity.name, + slug: entity.slug, + type_id: entity.type_id, + status: entity.status, + geometry_count: 0, + })); + + const mergedRows = mergeEntitySearchResults(rows, pendingMatches); + setEntitySearchResults(mergedRows); + setSelectedSearchEntityId((prev) => + prev && mergedRows.some((entity) => entity.id === prev) + ? prev + : mergedRows[0]?.id || null + ); + } catch (err) { + if (disposed || requestId !== entitySearchRequestRef.current) return; + console.error("Search entity by name failed", err); + const pendingMatches = pendingEntityCreates + .filter((entity) => + entity.name.toLowerCase().includes(keyword.toLowerCase()) || + (entity.slug || "").toLowerCase().includes(keyword.toLowerCase()) + ) + .map((entity) => ({ + id: entity.id, + name: entity.name, + slug: entity.slug, + type_id: entity.type_id, + status: entity.status, + geometry_count: 0, + })); + setEntitySearchResults(pendingMatches); + setSelectedSearchEntityId(pendingMatches[0]?.id || null); + } finally { + if (!disposed && requestId === entitySearchRequestRef.current) { + setIsEntitySearchLoading(false); + } + } + }, 220); + + return () => { + disposed = true; + window.clearTimeout(timeoutId); + }; + }, [ + entitySearchQuery, + selectedFeature, + pendingEntityCreates, + setEntitySearchResults, + setIsEntitySearchLoading, + setSelectedSearchEntityId, + ]); + + useEffect(() => { + if (selectedFeatureId === null) return; + const stillExists = editor.draft.features.some((feature) => + String(feature.properties.id) === String(selectedFeatureId) + ); + if (!stillExists) { + setSelectedFeatureId(null); + } + }, [editor.draft, selectedFeatureId, setSelectedFeatureId]); + + useEffect(() => { + if (!selectedFeature) { + setSelectedGeometryEntityIds([]); + setGeometryMetaForm({ + time_start: "", + time_end: "", + binding: "", + }); + setEntitySearchQuery(""); + setEntitySearchResults([]); + setSelectedSearchEntityId(null); + setEntityFormStatus(null); + return; + } + + const featureEntityIds = normalizeFeatureEntityIds(selectedFeature); + setSelectedGeometryEntityIds(featureEntityIds); + setGeometryMetaForm({ + time_start: selectedFeature.properties.time_start != null + ? String(selectedFeature.properties.time_start) + : "", + time_end: selectedFeature.properties.time_end != null + ? String(selectedFeature.properties.time_end) + : "", + binding: normalizeFeatureBindingIds(selectedFeature).join(", "), + }); + setEntitySearchQuery(""); + setEntitySearchResults([]); + setSelectedSearchEntityId(null); + setEntityFormStatus(null); + }, [ + selectedFeature, + setEntityFormStatus, + setEntitySearchQuery, + setEntitySearchResults, + setGeometryMetaForm, + setSelectedGeometryEntityIds, + setSelectedSearchEntityId, + ]); + + useEffect(() => { + if (!selectedFeature) return; + + const allowedGroupIds = getAllowedEntityTypeGroupIdsForFeature(selectedFeature); + const fallbackOption = ENTITY_TYPE_OPTIONS.find((option) => + allowedGroupIds.includes(option.groupId) + ); + if (!fallbackOption) return; + + setEntityForm((prev) => { + const currentOption = findEntityTypeOption(prev.type_id); + const isCurrentAllowed = currentOption + ? allowedGroupIds.includes(currentOption.groupId) + : false; + if (isCurrentAllowed || prev.type_id === fallbackOption.value) { + return prev; + } + return { + ...prev, + type_id: fallbackOption.value, + }; + }); + }, [selectedFeature, setEntityForm]); + + useEffect(() => { + const timeoutId = window.setTimeout(() => { + if (timelineDraftYear !== timelineYear) { + setTimelineYear(timelineDraftYear); + } + }, TIMELINE_DEBOUNCE_MS); + + return () => window.clearTimeout(timeoutId); + }, [timelineDraftYear, timelineYear, setTimelineYear]); + + useEffect(() => { + setBackgroundVisibility(loadBackgroundLayerVisibilityFromStorage()); + setIsBackgroundVisibilityReady(true); + }, [setBackgroundVisibility, setIsBackgroundVisibilityReady]); + + useEffect(() => { + if (activeSection) return; + + let disposed = false; + const requestId = ++timelineFetchRequestRef.current; + + async function loadGlobalByTimeline() { + setIsTimelineLoading(true); + setTimelineStatus(null); + + try { + const data = await fetchGeometriesByBBox({ + ...WORLD_BBOX, + time: timelineYear, + }); + + if (disposed || requestId !== timelineFetchRequestRef.current) return; + setInitialData(data); + } catch (err) { + if (err instanceof ApiError) { + console.error("Load global timeline data failed", err.body); + } else { + console.error("Load global timeline data failed", err); + } + + if (!disposed && requestId === timelineFetchRequestRef.current) { + setTimelineStatus("Không tải được geometry global tại mốc thời gian đã chọn."); + } + } finally { + if (!disposed && requestId === timelineFetchRequestRef.current) { + setIsTimelineLoading(false); + } + } + } + + loadGlobalByTimeline(); + + return () => { + disposed = true; + }; + }, [ + timelineYear, + activeSection, + setInitialData, + setIsTimelineLoading, + setTimelineStatus, + ]); + + const updateBackgroundVisibility = ( + updater: (prev: BackgroundLayerVisibility) => BackgroundLayerVisibility + ) => { + setBackgroundVisibility((prev) => { + const next = updater(prev); + persistBackgroundLayerVisibility(next); + return next; + }); + }; + + const handleToggleBackgroundLayer = (id: BackgroundLayerId) => { + updateBackgroundVisibility((prev) => ({ + ...prev, + [id]: !prev[id], + })); + }; + + const handleShowAllBackgroundLayers = () => { + updateBackgroundVisibility(() => ({ ...DEFAULT_BACKGROUND_LAYER_VISIBILITY })); + }; + + const handleHideAllBackgroundLayers = () => { + updateBackgroundVisibility(() => ({ ...HIDDEN_BACKGROUND_LAYER_VISIBILITY })); + }; + + const handleTimelineYearChange = (nextYear: number) => { + setTimelineDraftYear(clampYearToFixedRange(Math.trunc(nextYear))); + }; + + const handleEntityFormChange = (key: keyof EntityFormState, value: string) => { + setEntityForm((prev) => ({ ...prev, [key]: value })); + }; + + const handleGeometryMetaFormChange = (key: "time_start" | "time_end" | "binding", value: string) => { + setGeometryMetaForm((prev) => ({ ...prev, [key]: value })); + }; + + const handleEntityIdsChange = (values: string[]) => { + setSelectedGeometryEntityIds(uniqueEntityIds(values)); + }; + + const handleAddSelectedSearchEntity = () => { + const entityId = selectedSearchEntityId ? selectedSearchEntityId.trim() : ""; + if (!entityId.length) { + setEntityFormStatus("Hãy chọn một entity từ kết quả search trước."); + return; + } + + const next = uniqueEntityIds([...selectedGeometryEntityIds, entityId]); + setSelectedGeometryEntityIds(next); + setSelectedSearchEntityId(null); + setEntityFormStatus(null); + }; + + const featureCommands = useFeatureCommands({ + editor, + selectedFeature, + geometryMetaForm, + setGeometryMetaForm, + selectedGeometryEntityIds, + setSelectedGeometryEntityIds, + entities, + setIsEntitySubmitting, + setEntityFormStatus, + }); + + const handleCreateEntityOnly = async () => { + const name = entityForm.name.trim(); + if (!name) { + setEntityFormStatus("Tên entity là bắt buộc."); + return; + } + + const slug = entityForm.slug.trim() || null; + const typeId = entityForm.type_id || DEFAULT_ENTITY_TYPE_ID; + const normalizedName = name.toLowerCase(); + const duplicatedName = entities.some((entity) => entity.name.trim().toLowerCase() === normalizedName); + if (duplicatedName) { + setEntityFormStatus("Tên entity đã tồn tại."); + return; + } + if (slug) { + const normalizedSlug = slug.toLowerCase(); + const duplicatedSlug = entities.some((entity) => + (entity.slug || "").trim().toLowerCase() === normalizedSlug + ); + if (duplicatedSlug) { + setEntityFormStatus("Slug entity đã tồn tại."); + return; + } + } + + const entityId = buildClientEntityId(); + const pendingCreate: PendingEntityCreate = { + id: entityId, + name, + slug, + type_id: typeId, + status: 1, + }; + + setIsEntitySubmitting(true); + setEntityFormStatus(null); + try { + setPendingEntityCreates((prev) => [pendingCreate, ...prev]); + setCreatedEntities((prev) => { + if (prev.some((item) => item.id === pendingCreate.id)) return prev; + return [ + { + id: pendingCreate.id, + name: pendingCreate.name, + type_id: pendingCreate.type_id || null, + }, + ...prev, + ]; + }); + + setEntityForm((prev) => ({ + ...prev, + name: "", + slug: "", + })); + setEntityStatus(null); + setEntityFormStatus("Đã thêm entity mới vào danh sách chờ Commit."); + + if (selectedFeature) { + setEntitySearchQuery(pendingCreate.name); + setSelectedSearchEntityId(pendingCreate.id); + } + } finally { + setIsEntitySubmitting(false); + } + }; + + const pendingSaveCount = editor.changeCount + pendingEntityCreates.length; + const headCommit = sectionState?.head_commit_id + ? sectionCommits.find((commit) => commit.id === sectionState.head_commit_id) || null + : null; + const timelineDisabled = isSaving || pendingSaveCount > 0; + const timelineStatusText = + pendingSaveCount > 0 + ? "Commit hoặc Undo hết thay đổi trước khi đổi mốc thời gian." + : isSaving + ? "Đang lưu thay đổi..." + : timelineStatus; + + const handleCreateFeature = (feature: Feature) => { + editor.createFeature(feature); + setSelectedFeatureId(feature.properties.id); + }; + + return ( +
+ + +
+ {isBackgroundVisibilityReady ? ( + + ) : ( +
+ )} + +
+ + + } + /> +
+ ); +} + +function normalizeEditorUserId(value: string): string { + const normalized = value.trim(); + return normalized || DEFAULT_EDITOR_USER_ID; +} + +function formatCommitTitle(commit: SectionCommit): string { + return commit.edit_summary?.trim() || `Commit ${commit.id.slice(0, 8)}`; +} + +function getAllowedEntityTypeGroupIdsForFeature(feature: Feature): EntityTypeGroupId[] { + const defaultTypeId = getDefaultTypeIdForFeature(feature); + const defaultTypeOption = findEntityTypeOption(defaultTypeId); + if (defaultTypeOption) { + return [defaultTypeOption.groupId]; + } + return ["polygon"]; +} diff --git a/src/app/editor/page.tsx b/src/app/editor/page.tsx new file mode 100644 index 0000000..cff2e76 --- /dev/null +++ b/src/app/editor/page.tsx @@ -0,0 +1,7 @@ +import { redirect } from "next/navigation"; + +export default function EditorIndexPage() { + // Editor must be opened from a specific project (see /user/projects). + redirect("/user/projects"); +} + diff --git a/src/app/user/projects/[id]/page.tsx b/src/app/user/projects/[id]/page.tsx index 365de43..5c2bc7d 100644 --- a/src/app/user/projects/[id]/page.tsx +++ b/src/app/user/projects/[id]/page.tsx @@ -10,6 +10,7 @@ import Swal from "sweetalert2"; import PageBreadcrumb from "@/components/common/PageBreadCrumb"; import { apiAddProjectMember, apiChangeProjectOwner, apiDeleteProject, apiGetProjectDetail, apiRemoveProjectMember, apiUpdateProject, apiUpdateProjectMemberRole } from "@/service/projectService"; import Loading from "@/app/loading"; +import Button from "@/components/ui/button/Button"; type TabType = "overview" | "members" | "settings"; @@ -256,7 +257,7 @@ export default function ProjectDetailsPage() {
-
+
{[ { id: "overview", @@ -305,6 +306,11 @@ export default function ProjectDetailsPage() { )} ))} + +
+
diff --git a/src/app/user/projects/page.tsx b/src/app/user/projects/page.tsx index 7ee2f43..2f4553e 100644 --- a/src/app/user/projects/page.tsx +++ b/src/app/user/projects/page.tsx @@ -217,7 +217,15 @@ export default function ProjectsPage() { -
+
+ +
{project.members && project.members.length > 0 ? ( <> @@ -330,4 +338,4 @@ export default function ProjectsPage() {
); -} \ No newline at end of file +} diff --git a/src/app/user/submissions/[id]/page.tsx b/src/app/user/submissions/[id]/page.tsx new file mode 100644 index 0000000..776afd4 --- /dev/null +++ b/src/app/user/submissions/[id]/page.tsx @@ -0,0 +1,331 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import { useParams } from "next/navigation"; +import { toast } from "sonner"; + +import PageBreadcrumb from "@/components/common/PageBreadCrumb"; +import ComponentCard from "@/components/common/ComponentCard"; +import Badge from "@/components/ui/badge/Badge"; +import Button from "@/components/ui/button/Button"; + +import Map from "@/uhm/components/Map"; +import { DEFAULT_BACKGROUND_LAYER_VISIBILITY } from "@/uhm/lib/backgroundLayers"; +import { EMPTY_FEATURE_COLLECTION } from "@/uhm/lib/geo/constants"; +import { fetchSectionCommits } from "@/uhm/api/sections"; +import type { EditorSnapshot, SectionCommit } from "@/uhm/types/sections"; +import type { EntitySnapshot } from "@/uhm/types/entities"; + +import type { Submission } from "@/interface/submission"; +import { apiGetSubmissionById } from "@/service/submissionService"; +import type { Project } from "@/interface/project"; +import { apiGetProjectDetail } from "@/service/projectService"; + +function formatTime(value?: string | null) { + if (!value) return "-"; + const d = new Date(value); + if (Number.isNaN(d.getTime())) return String(value); + return d.toLocaleString("vi-VN"); +} + +export default function SubmissionDetailPage() { + const params = useParams(); + const id = String(params.id || ""); + const [row, setRow] = useState(null); + const [project, setProject] = useState(null); + const [commits, setCommits] = useState([]); + const [snapshot, setSnapshot] = useState(null); + const [snapshotEntities, setSnapshotEntities] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [isLoadingExtras, setIsLoadingExtras] = useState(false); + + const headCommitSnapshotJson = useMemo(() => { + const headId = project?.latest_commit_id || null; + if (!headId) return null; + const head = commits.find((c) => c.id === headId) || null; + return (head as any)?.snapshot_json ?? null; + }, [commits, project?.latest_commit_id]); + + const draft = useMemo( + () => snapshot?.editor_feature_collection || EMPTY_FEATURE_COLLECTION, + [snapshot] + ); + + useEffect(() => { + let disposed = false; + async function load() { + if (!id) return; + try { + setIsLoading(true); + const res = await apiGetSubmissionById(id); + if (!disposed) setRow(res?.data || null); + } catch (err) { + console.error(err); + toast.error("Khong the tai submission."); + } finally { + if (!disposed) setIsLoading(false); + } + } + load(); + return () => { + disposed = true; + }; + }, [id]); + + useEffect(() => { + let disposed = false; + async function loadExtras() { + if (!row?.project_id) return; + try { + setIsLoadingExtras(true); + + const [projectRes, commitRows] = await Promise.all([ + apiGetProjectDetail(row.project_id), + fetchSectionCommits(row.project_id), + ]); + + if (disposed) return; + setProject(projectRes?.data || null); + setCommits(commitRows || []); + + const commit = (commitRows || []).find((c) => c.id === row.commit_id) || null; + const snap = (commit?.snapshot_json || null) as EditorSnapshot | null; + setSnapshot(snap); + setSnapshotEntities((snap?.entities || []) as EntitySnapshot[]); + } catch (err) { + console.error(err); + toast.error("Khong the tai thong tin project/commit."); + } finally { + if (!disposed) setIsLoadingExtras(false); + } + } + loadExtras(); + return () => { + disposed = true; + }; + }, [row?.commit_id, row?.project_id]); + + return ( +
+ + +
+ + {isLoading ? ( +
Dang tai...
+ ) : row ? ( +
+
+
ID
+
{row.id}
+
+
+
Status
+
+ + {row.status} + +
+
+
+
Project
+
{row.project_title || "-"}
+
{row.project_id}
+
+
+
Commit
+
{row.commit_id}
+
+
+
User
+
{row.user_id}
+
+
+
Created
+
{formatTime(row.created_at)}
+
+
+
Reviewed by
+
{row.reviewed_by || "-"}
+
+
+
Reviewed at
+
{formatTime(row.reviewed_at)}
+
+
+
Review note
+
{row.review_note || "-"}
+
+
+
Content
+
{row.content || "-"}
+
+ +
+ +
+
+ ) : ( +
Khong tim thay submission.
+ )} +
+
+ + {row ? ( +
+ +
+
+ {}} + backgroundVisibility={DEFAULT_BACKGROUND_LAYER_VISIBILITY} + allowGeometryEditing={false} + respectBindingFilter={false} + height="320px" + fitToDraftBounds + fitBoundsKey={row.id} + /> +
+ {isLoadingExtras ? ( +
Dang tai snapshot/commits...
+ ) : snapshot ? ( +
+ Snapshot schema_version: {snapshot.schema_version} +
+ ) : ( +
+ Khong tim thay snapshot cho commit nay. +
+ )} +
+
+ + +
+ {snapshotEntities.length === 0 ? ( +
Khong co entities trong snapshot.
+ ) : ( +
+
+
+
Op
+
Name
+
Entity ID
+
+
+ {snapshotEntities.map((e) => ( +
+
+ + {e.operation} + +
+
{e.name || "-"}
+
{e.id}
+
+ ))} +
+
+
+ )} +
+
+
+ ) : null} + + {row ? ( +
+ +
+
+                {JSON.stringify(headCommitSnapshotJson, null, 2)}
+              
+
+
+ + +
+ {!project ? ( +
Khong co du lieu project.
+ ) : (project.members || []).length === 0 ? ( +
Khong co thanh vien.
+ ) : ( +
+
+
+
Member
+
Role
+
User ID
+
+
+ {(project.members || []).map((m) => ( +
+
{m.display_name || "-"}
+
+ + {m.role} + +
+
{m.user_id}
+
+ ))} +
+
+
+ )} +
+
+
+ ) : null} + + {row ? ( +
+ +
+ {commits.length === 0 ? ( +
Khong co commits.
+ ) : ( +
+
+
+
Commit
+
Title
+
Created
+
User
+
+
+ {commits.map((c) => { + const isTarget = c.id === row.commit_id; + return ( +
+
+ {isTarget ? {c.id} : c.id} +
+
{c.edit_summary || "-"}
+
{formatTime(c.created_at)}
+
{c.user_id}
+
+ ); + })} +
+
+
+ )} +
+
+
+ ) : null} +
+ ); +} diff --git a/src/app/user/submissions/page.tsx b/src/app/user/submissions/page.tsx new file mode 100644 index 0000000..d6ac461 --- /dev/null +++ b/src/app/user/submissions/page.tsx @@ -0,0 +1,326 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import Link from "next/link"; +import { toast } from "sonner"; + +import PageBreadcrumb from "@/components/common/PageBreadCrumb"; +import ComponentCard from "@/components/common/ComponentCard"; +import Badge from "@/components/ui/badge/Badge"; +import Button from "@/components/ui/button/Button"; +import Label from "@/components/form/Label"; +import { Modal } from "@/components/ui/modal"; +import { useModal } from "@/hooks/useModal"; + +import type { Submission, SubmissionStatus } from "@/interface/submission"; +import { apiSearchSubmissions, apiUpdateSubmissionStatus } from "@/service/submissionService"; + +type Decision = "APPROVED" | "REJECTED"; + +function statusBadge(status: SubmissionStatus) { + switch (status) { + case "PENDING": + return ( + + PENDING + + ); + case "APPROVED": + return ( + + APPROVED + + ); + case "REJECTED": + return ( + + REJECTED + + ); + default: + return ( + + {String(status || "UNKNOWN")} + + ); + } +} + +function formatTime(value?: string | null) { + if (!value) return "-"; + const d = new Date(value); + if (Number.isNaN(d.getTime())) return String(value); + return d.toLocaleString("vi-VN"); +} + +export default function SubmissionsPage() { + const [items, setItems] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [page, setPage] = useState(1); + const limit = 20; + const [totalPages, setTotalPages] = useState(1); + + const [search, setSearch] = useState(""); + const [projectId, setProjectId] = useState(""); + const [status, setStatus] = useState<"ALL" | "PENDING" | "APPROVED" | "REJECTED">("PENDING"); + + const { isOpen, openModal, closeModal } = useModal(); + const [active, setActive] = useState(null); + const [decision, setDecision] = useState("APPROVED"); + const [reviewNote, setReviewNote] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + + const query = useMemo(() => { + const trimmedSearch = search.trim(); + const trimmedProject = projectId.trim(); + return { + page, + limit, + project_id: trimmedProject.length ? trimmedProject : undefined, + search: trimmedSearch.length ? trimmedSearch : undefined, + statuses: status === "ALL" ? undefined : ([status] as any), + sort: "created_at" as const, + }; + }, [limit, page, projectId, search, status]); + + const fetchList = async () => { + try { + setIsLoading(true); + const res = await apiSearchSubmissions(query); + const payload = res?.data; + const rows = payload?.data || []; + setItems(rows); + setTotalPages(payload?.pagination?.total_pages || 1); + } catch (err) { + console.error(err); + toast.error("Khong the tai danh sach submissions."); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchList(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [query]); + + const openReview = (row: Submission, nextDecision: Decision) => { + setActive(row); + setDecision(nextDecision); + setReviewNote(""); + openModal(); + }; + + const submitDecision = async () => { + if (!active) return; + const note = reviewNote.trim(); + if (note.length < 10) { + toast.error("Review note toi thieu 10 ky tu."); + return; + } + + try { + setIsSubmitting(true); + await apiUpdateSubmissionStatus(active.id, { status: decision, review_note: note }); + toast.success(decision === "APPROVED" ? "Da duyet submission." : "Da tu choi submission."); + closeModal(); + await fetchList(); + } catch (err: any) { + console.error(err); + toast.error(err?.response?.data?.message || "Cap nhat trang thai that bai."); + } finally { + setIsSubmitting(false); + } + }; + + return ( +
+ + +
+ +
+
+ + { + setPage(1); + setSearch(e.target.value); + }} + placeholder="Tim theo keyword (>= 2 ky tu)" + className="h-11 w-full rounded-xl border border-gray-200 bg-transparent px-4 py-2.5 text-sm text-gray-800 outline-none focus:border-brand-300 focus:ring-3 focus:ring-brand-500/10 dark:border-gray-800 dark:text-white/90 dark:focus:border-brand-800" + /> +
+
+ + { + setPage(1); + setProjectId(e.target.value); + }} + placeholder="UUID" + className="h-11 w-full rounded-xl border border-gray-200 bg-transparent px-4 py-2.5 text-sm text-gray-800 outline-none focus:border-brand-300 focus:ring-3 focus:ring-brand-500/10 dark:border-gray-800 dark:text-white/90 dark:focus:border-brand-800" + /> +
+
+ + +
+
+ +
+ {isLoading ? ( +
+
+
+ ) : null} + +
+
+
+
Project
+
Submitter
+
Status
+
Created
+
Actions
+
+ +
+ {items.length === 0 ? ( +
Khong co submissions.
+ ) : null} + {items.map((row) => ( +
{ + const target = e.target as HTMLElement | null; + if (target && target.closest("button")) return; + window.location.href = `/user/submissions/${row.id}`; + }} + role="link" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === "Enter") window.location.href = `/user/submissions/${row.id}`; + }} + > +
+
+ {row.project_title || row.project_id} +
+
+ Submission:{" "} + + {row.id} + +
+
+ Commit: {row.commit_id} +
+
+
+ {row.user?.display_name || row.user?.email || row.user_id} +
+
{statusBadge(row.status)}
+
{formatTime(row.created_at)}
+
+ + {row.status === "PENDING" ? ( + <> + + + + ) : ( + + )} +
+
+ ))} +
+
+
+ +
+
+ Page {page} / {totalPages} +
+
+ + +
+
+
+ +
+ + +
+

+ {decision === "APPROVED" ? "Duyet submission" : "Tu choi submission"} +

+
+ {active?.id} +
+ +
+
+ +