diff --git a/next.config.ts b/next.config.ts index 10ba076..4318e1f 100644 --- a/next.config.ts +++ b/next.config.ts @@ -19,34 +19,6 @@ const nextConfig: NextConfig = { ], }, output: 'standalone', - async rewrites() { - // Proxy backend API calls through Next.js to avoid browser CORS issues. - // - // Configure the target via: - // - API_PROXY_TARGET (server-side, recommended) e.g. http://localhost:8080 - // - NEXT_PUBLIC_API_URL_ROOT (fallback) - const target = - process.env.API_PROXY_TARGET || - process.env.NEXT_PUBLIC_API_URL_ROOT || - "https://history-api.kain.id.vn"; - - const prefixes = [ - "auth", - "users", - "media", - "projects", - "submissions", - "statistics", - "roles", - "historian", - ]; - return [ - ...prefixes.map((p) => ({ - source: `/${p}/:path*`, - destination: `${target}/${p}/:path*`, - })), - ]; - }, webpack(config) { config.module.rules.push({ test: /\.svg$/, diff --git a/package-lock.json b/package-lock.json index 439f02d..7aaf09b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,10 @@ "autoprefixer": "^10.4.22", "axios": "^1.14.0", "flatpickr": "^4.6.13", + "maplibre-gl": "^5.24.0", "next": "^16.1.6", + "polylabel": "^2.0.1", + "quill-blot-formatter": "^1.0.5", "react": "^19.2.0", "react-apexcharts": "^1.8.0", "react-dnd": "^16.0.1", @@ -36,12 +39,15 @@ "sweetalert2": "^11.26.24", "swiper": "^11.2.10", "tailwind-merge": "^2.6.0", - "yet-another-react-lightbox": "^3.30.1" + "uuid": "^13.0.2", + "yet-another-react-lightbox": "^3.30.1", + "zustand": "^5.0.14" }, "devDependencies": { "@eslint/eslintrc": "^3.3.1", "@svgr/webpack": "^8.1.0", "@types/node": "^20.19.25", + "@types/polylabel": "^1.1.3", "@types/react": "^19.2.1", "@types/react-dom": "^19.2.1", "@types/react-transition-group": "^4.4.12", @@ -2564,6 +2570,118 @@ "@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.2.0", + "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.2.0.tgz", + "integrity": "sha512-LVL4wgI9YAum5V+LNVQO6QgFBPw7/MIIY4XJPNsPDMrjEwcE+JfKk1LuIl8GnF197ejVdC9QdPaxrx5gfgdGXg==", + "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.5", + "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-2.0.5.tgz", + "integrity": "sha512-pXj8m7KTsqZt+1jsE0xIpGvqTSbblfkuEJL/NJmNePMtEwxO8V3XMDo9WMSfDeqHvCtBI9Lmt4mGcGR10zecmw==", + "license": "BSD-3-Clause", + "dependencies": { + "@mapbox/point-geometry": "~1.1.0", + "@types/geojson": "^7946.0.16", + "pbf": "^4.0.2" + } + }, + "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.10.0", + "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-24.10.0.tgz", + "integrity": "sha512-lichxSiagMEBBrqHF0trtMQH9RKh+9jUlIJl0qW0QHvt2H/tbvUWdE+ZzI2Jd0/pT7j/iavLonlPu7EQ/ixTOw==", + "license": "ISC", + "dependencies": { + "@mapbox/jsonlint-lines-primitives": "~2.0.2", + "@mapbox/unitbezier": "^1.0.0", + "json-stringify-pretty-compact": "^4.0.0", + "minimist": "^1.2.8", + "quickselect": "^3.0.0", + "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/maplibre-gl-style-spec/node_modules/@mapbox/unitbezier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-1.0.0.tgz", + "integrity": "sha512-fqd515fjBmANKGGsQ286E2Wvj/XvDFpGzwJxq4CI6jMQue6Oy04uCKp+JWKF00xRTmk6cEu1jPJ9p3xqH8YWqQ==", + "license": "BSD-2-Clause" + }, + "node_modules/@maplibre/mlt": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@maplibre/mlt/-/mlt-1.1.11.tgz", + "integrity": "sha512-dKvjKdITw9d0y3ndGkSqLUEpWCizMtdq8NB06cHohH/JZ2sJoM7dClR9wzJLUWykjbw9RXDFmhjjNBnNW27mzw==", + "license": "(MIT OR Apache-2.0)", + "dependencies": { + "@mapbox/point-geometry": "^1.1.0" + } + }, + "node_modules/@maplibre/vt-pbf": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@maplibre/vt-pbf/-/vt-pbf-4.3.2.tgz", + "integrity": "sha512-j6p0AdjvAR19Z3XaCysle7A4ZSo08tYOzxD0Y9NQylwPAkwJJeYub5b2eVucdeDh7erhv69DahoLOevDRERRUw==", + "license": "MIT", + "dependencies": { + "@mapbox/point-geometry": "^1.1.0", + "@types/geojson": "^7946.0.16", + "pbf": "^5.1.0" + } + }, + "node_modules/@maplibre/vt-pbf/node_modules/pbf": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-5.1.0.tgz", + "integrity": "sha512-Wv0yo0+uZepnoNEKsquhar1F18LogB8oeEikIhUXG16udbiXG7JecHGySwoo6kuMgjmbQYzdrTZlO+/K9t8eZg==", + "license": "BSD-3-Clause", + "dependencies": { + "resolve-protobuf-schema": "^2.1.0" + }, + "bin": { + "pbf": "bin/pbf" + } + }, "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", @@ -3494,6 +3612,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", @@ -3519,6 +3643,13 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/polylabel": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/polylabel/-/polylabel-1.1.3.tgz", + "integrity": "sha512-9Zw2KoDpi+T4PZz2G6pO2xArE0m/GSMTW1MIxF2s8ZY8x9XDO6fv9um0ydRGvcbkFLlaq8yNK6eZxnmMZtDgWQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", @@ -4622,7 +4753,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.0", @@ -4654,7 +4784,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -4739,6 +4868,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", @@ -5017,6 +5155,26 @@ } } }, + "node_modules/deep-equal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz", + "integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==", + "license": "MIT", + "dependencies": { + "is-arguments": "^1.1.1", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.5.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -5038,7 +5196,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", @@ -5056,7 +5213,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, "license": "MIT", "dependencies": { "define-data-property": "^1.0.1", @@ -5205,6 +5361,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", @@ -5883,9 +6045,15 @@ } }, "node_modules/eventemitter3": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz", + "integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==", + "license": "MIT" + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, "node_modules/fast-deep-equal": { @@ -6135,7 +6303,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6229,6 +6396,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", @@ -6317,7 +6490,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" @@ -6469,6 +6641,22 @@ "node": ">= 0.4" } }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -6621,7 +6809,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -6750,7 +6937,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -6999,6 +7185,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", @@ -7028,6 +7220,12 @@ "node": ">=4.0" } }, + "node_modules/kdbush": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.1.0.tgz", + "integrity": "sha512-e9vurzrXJQrFX6ckpHP3bvj5l+9CnYzkxDNnNQ1h2QTqdWsUAJgXiKdGNcOa1EY85dU8KbQ+z/FdQdB7P+9yfQ==", + "license": "ISC" + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -7418,6 +7616,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", @@ -7505,7 +7737,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" @@ -7518,6 +7749,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", @@ -7711,11 +7948,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7880,9 +8132,9 @@ } }, "node_modules/parchment": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/parchment/-/parchment-3.0.0.tgz", - "integrity": "sha512-HUrJFQ/StvgmXRcQ1ftY6VEZUq3jA2t9ncFN4F84J/vN0/FPpQF+8FKXb3l6fLces6q0uOHj6NJn+2xvZnxO6A==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz", + "integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==", "license": "BSD-3-Clause" }, "node_modules/parent-module": { @@ -7954,6 +8206,18 @@ "node": ">=8" } }, + "node_modules/pbf": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.2.tgz", + "integrity": "sha512-J0ajxARhZfpUEebxYs1vhMGMuLSXtBe1e+fFPDrf2uA2hgo+UshKfNUWOz92HJNz6/NFEXseQPddnHkTreWRqg==", + "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", @@ -7973,6 +8237,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/polylabel": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/polylabel/-/polylabel-2.0.1.tgz", + "integrity": "sha512-B6Yu+Bdl/8SGtjVhyUfZzD3DwciCS9SPVtHiNdt8idHHatvTHp5Ss8XGDRmQFtfF1ZQnfK+Cj5dXdpkUXBbXgA==", + "license": "ISC", + "dependencies": { + "tinyqueue": "^3.0.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -8018,6 +8291,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", @@ -8049,6 +8328,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", @@ -8089,19 +8374,45 @@ ], "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", - "integrity": "sha512-xEYQBqfYx/sfb33VJiKnSJp8ehloavImQ2A6564GAbqG55PGw1dAWUn1MUbQB62t0azawUS2CZZhWCjO8gRvTw==", + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/quill/-/quill-1.3.7.tgz", + "integrity": "sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==", "license": "BSD-3-Clause", "dependencies": { - "eventemitter3": "^5.0.1", - "lodash-es": "^4.17.21", - "parchment": "^3.0.0", - "quill-delta": "^5.1.0" + "clone": "^2.1.1", + "deep-equal": "^1.0.1", + "eventemitter3": "^2.0.3", + "extend": "^3.0.2", + "parchment": "^1.1.4", + "quill-delta": "^3.6.2" + } + }, + "node_modules/quill-blot-formatter": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/quill-blot-formatter/-/quill-blot-formatter-1.0.5.tgz", + "integrity": "sha512-iVmuEdmMIpvERBnnDfosWul6VAVN6tqQRruUzAEwA9ZbQ/Ef7DTHGZDUR4KklXpxM+z50opFp6m1NhNdN6HJhw==", + "license": "Apache-2.0", + "dependencies": { + "deepmerge": "^2.0.0" }, + "peerDependencies": { + "quill": "^1.3.4" + } + }, + "node_modules/quill-blot-formatter/node_modules/deepmerge": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", + "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==", + "license": "MIT", "engines": { - "npm": ">=8.2.3" + "node": ">=0.10.0" } }, "node_modules/quill-delta": { @@ -8119,6 +8430,26 @@ "node": ">= 12.0.0" } }, + "node_modules/quill/node_modules/fast-diff": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz", + "integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==", + "license": "Apache-2.0" + }, + "node_modules/quill/node_modules/quill-delta": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz", + "integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==", + "license": "MIT", + "dependencies": { + "deep-equal": "^1.0.1", + "extend": "^3.0.2", + "fast-diff": "1.1.2" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/react": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", @@ -8232,6 +8563,33 @@ "react-dom": "^16 || ^17 || ^18 || ^19" } }, + "node_modules/react-quill-new/node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/react-quill-new/node_modules/parchment": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/parchment/-/parchment-3.0.0.tgz", + "integrity": "sha512-HUrJFQ/StvgmXRcQ1ftY6VEZUq3jA2t9ncFN4F84J/vN0/FPpQF+8FKXb3l6fLces6q0uOHj6NJn+2xvZnxO6A==", + "license": "BSD-3-Clause" + }, + "node_modules/react-quill-new/node_modules/quill": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/quill/-/quill-2.0.3.tgz", + "integrity": "sha512-xEYQBqfYx/sfb33VJiKnSJp8ehloavImQ2A6564GAbqG55PGw1dAWUn1MUbQB62t0azawUS2CZZhWCjO8gRvTw==", + "license": "BSD-3-Clause", + "dependencies": { + "eventemitter3": "^5.0.1", + "lodash-es": "^4.17.21", + "parchment": "^3.0.0", + "quill-delta": "^5.1.0" + }, + "engines": { + "npm": ">=8.2.3" + } + }, "node_modules/react-redux": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", @@ -8319,7 +8677,6 @@ "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -8421,6 +8778,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", @@ -8541,7 +8907,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", @@ -8559,7 +8924,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", @@ -9120,6 +9484,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", @@ -9462,6 +9832,19 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/uuid": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.2.tgz", + "integrity": "sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -9646,6 +10029,35 @@ "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } + }, + "node_modules/zustand": { + "version": "5.0.14", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz", + "integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index 95b8e0a..5a9c93b 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,10 @@ "autoprefixer": "^10.4.22", "axios": "^1.14.0", "flatpickr": "^4.6.13", + "maplibre-gl": "^5.24.0", "next": "^16.1.6", + "polylabel": "^2.0.1", + "quill-blot-formatter": "^1.0.5", "react": "^19.2.0", "react-apexcharts": "^1.8.0", "react-dnd": "^16.0.1", @@ -38,12 +41,15 @@ "sweetalert2": "^11.26.24", "swiper": "^11.2.10", "tailwind-merge": "^2.6.0", - "yet-another-react-lightbox": "^3.30.1" + "uuid": "^13.0.2", + "yet-another-react-lightbox": "^3.30.1", + "zustand": "^5.0.14" }, "devDependencies": { "@eslint/eslintrc": "^3.3.1", "@svgr/webpack": "^8.1.0", "@types/node": "^20.19.25", + "@types/polylabel": "^1.1.3", "@types/react": "^19.2.1", "@types/react-dom": "^19.2.1", "@types/react-transition-group": "^4.4.12", diff --git a/src/app/(admin)/(others-pages)/submissions/page.tsx b/src/app/(admin)/(others-pages)/submissions/page.tsx index c35b240..7322780 100644 --- a/src/app/(admin)/(others-pages)/submissions/page.tsx +++ b/src/app/(admin)/(others-pages)/submissions/page.tsx @@ -197,7 +197,7 @@ export default function Page() { }; const handleViewDetails = (id: string) => { - router.push(`/submissions/${id}`); + window.open(`/submissions/${id}`, "_blank"); }; const handleOpenActionModal = (item: SubmissionItem) => { diff --git a/src/app/(full-width-pages)/submissions/[id]/page.tsx b/src/app/(full-width-pages)/submissions/[id]/page.tsx new file mode 100644 index 0000000..8a4cb48 --- /dev/null +++ b/src/app/(full-width-pages)/submissions/[id]/page.tsx @@ -0,0 +1,813 @@ +"use client"; + +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useParams, useRouter } from "next/navigation"; +import { useShallow } from "zustand/react/shallow"; +import { toast } from "sonner"; + +import Map from "@/uhm/components/Map"; +import BackgroundLayersPanel from "@/uhm/components/editor/BackgroundLayersPanel"; +import TimelineBar from "@/uhm/components/ui/TimelineBar"; +import ProjectEntityRefsPanel from "@/uhm/components/editor/ProjectEntityRefsPanel"; +import EntityWikiBindingsPanel from "@/uhm/components/editor/EntityWikiBindingsPanel"; +import GeometryBindingPanel from "@/uhm/components/editor/GeometryBindingPanel"; +import SelectedGeometryPanel from "@/uhm/components/editor/SelectedGeometryPanel"; +import WikiSidebarPanel from "@/uhm/components/wiki/WikiSidebarPanel"; + +import { fetchProjectCommits } from "@/uhm/api/projects"; +import { requestJson } from "@/uhm/api/http"; +import { API_ENDPOINTS } from "@/uhm/api/config"; +import { normalizeEditorSnapshot, getDefaultTypeIdForFeature, normalizeFeatureEntityIds } from "@/uhm/lib/editor/snapshot/editorSnapshot"; +import { normalizeTimelineYearValue } from "@/uhm/lib/utils/timeline"; +import { isFeatureVisibleAtYear } from "@/uhm/lib/editor/editorPageUtils"; +import { buildEntityLabelContextDraft as buildPreviewEntityLabelContextDraft } from "@/uhm/lib/preview/relationIndex"; +import { getDirectGeometryChildIds } from "@/uhm/lib/editor/geometry/geometryBinding"; +import { ResizeHandle } from "@/uhm/components/ui/ResizeHandle"; +import { EMPTY_FEATURE_COLLECTION, WORLD_BBOX } from "@/uhm/lib/map/geo/constants"; +import { FIXED_TIMELINE_RANGE, clampYearToFixedRange } from "@/uhm/lib/utils/timeline"; +import { loadBackgroundLayerVisibilityFromStorage } from "@/uhm/lib/editor/background/backgroundVisibilityStorage"; + + +import { apiGetSubmissionDetail, updateSubmission } from "@/service/submisisonService"; +import { SubmissionItem } from "@/components/tables/SubmissionsTable"; +import { + EditorStoreProvider, + useEditorStore, + useEditorStoreApi, +} from "@/uhm/store/editorStore"; + +import type { Feature, FeatureCollection, GeometryEntitySnapshot, GeometrySnapshot } from "@/uhm/types/geo"; +import type { EditorSnapshot, ProjectCommit, EntityWikiLinkSnapshot, Project } from "@/uhm/types/projects"; +import type { EntitySnapshot } from "@/uhm/types/entities"; +import type { WikiSnapshot } from "@/uhm/types/wiki"; + +const CURRENT_YEAR = new Date().getUTCFullYear(); +const DEFAULT_EDITOR_USER_ID = "admin-viewer"; + +// Helper functions copied from useProjectCommands.ts to build read-only session snapshot. +function toEditorSessionSnapshot(snapshot: EditorSnapshot): EditorSnapshot { + return { + ...snapshot, + entities: toEditorSessionEntities(snapshot.entities), + geometries: toEditorSessionGeometries(snapshot.geometries), + geometry_entity: toEditorSessionGeometryEntity(snapshot.geometry_entity), + wikis: toEditorSessionWikis(snapshot.wikis), + entity_wiki: toEditorSessionEntityWikiLinks(snapshot.entity_wiki), + }; +} + +function toEditorSessionEntities(input: EditorSnapshot["entities"]): EntitySnapshot[] { + const rows = Array.isArray(input) ? input : []; + return rows + .filter((e): e is any => Boolean(e) && (typeof e.id === "string" || typeof e.id === "number")) + .filter((e) => e.operation !== "delete") + .map((e) => { + const id = String(e.id); + const source: EntitySnapshot["source"] = e.source === "inline" ? "inline" : "ref"; + return { + id, + source, + operation: "reference", + name: typeof e.name === "string" ? e.name : undefined, + description: typeof e.description === "string" ? e.description : e.description ?? null, + time_start: normalizeTimelineYearValue(e.time_start) ?? undefined, + time_end: normalizeTimelineYearValue(e.time_end) ?? undefined, + }; + }); +} + +function toEditorSessionGeometries(input: EditorSnapshot["geometries"]): GeometrySnapshot[] { + const rows = Array.isArray(input) ? input : []; + return rows + .filter((g): g is any => Boolean(g) && (typeof g.id === "string" || typeof g.id === "number")) + .filter((g) => g.operation !== "delete") + .map((g) => { + const id = String(g.id); + const source: GeometrySnapshot["source"] = g.source === "inline" ? "inline" : "ref"; + return { + id, + source, + operation: "reference", + type: g.type ?? undefined, + draw_geometry: g.draw_geometry, + geometry: g.geometry, + bound_with: g.bound_with ?? null, + time_start: normalizeTimelineYearValue(g.time_start) ?? undefined, + time_end: normalizeTimelineYearValue(g.time_end) ?? undefined, + bbox: g.bbox + ? { + min_lng: g.bbox.min_lng, + min_lat: g.bbox.min_lat, + max_lng: g.bbox.max_lng, + max_lat: g.bbox.max_lat, + } + : g.bbox ?? undefined, + }; + }); +} + +function toEditorSessionGeometryEntity(input: EditorSnapshot["geometry_entity"]): GeometryEntitySnapshot[] { + const rows = Array.isArray(input) ? input : []; + const deduped = new globalThis.Map(); + for (const row of rows) { + if (!row) continue; + const safeRow = row as any; + if (safeRow.operation === "delete") continue; + const geometry_id = typeof safeRow.geometry_id === "string" || typeof safeRow.geometry_id === "number" + ? String(safeRow.geometry_id).trim() + : ""; + const entity_id = typeof safeRow.entity_id === "string" || typeof safeRow.entity_id === "number" + ? String(safeRow.entity_id).trim() + : ""; + if (!geometry_id || !entity_id) continue; + const key = `${geometry_id}::${entity_id}`; + deduped.set(key, { + geometry_id, + entity_id, + operation: "reference", + }); + } + return Array.from(deduped.values()).sort((a, b) => { + const g = a.geometry_id.localeCompare(b.geometry_id); + if (g !== 0) return g; + return a.entity_id.localeCompare(b.entity_id); + }); +} + +function toEditorSessionWikis(input: EditorSnapshot["wikis"]): WikiSnapshot[] { + const rows = Array.isArray(input) ? input : []; + return rows + .filter((w): w is any => Boolean(w) && typeof w.id === "string" && w.id.trim().length > 0) + .filter((w) => w.operation !== "delete") + .map((w) => { + const source: WikiSnapshot["source"] = w.source === "inline" ? "inline" : "ref"; + return { + id: w.id, + source, + operation: "reference", + title: typeof w.title === "string" ? w.title : "", + slug: w.slug ?? null, + doc: w.doc ?? null, + }; + }); +} + +function toEditorSessionEntityWikiLinks(input: EditorSnapshot["entity_wiki"]): EntityWikiLinkSnapshot[] { + const rows = Array.isArray(input) ? input : []; + const deduped = new globalThis.Map(); + for (const row of rows) { + if (!row) continue; + const safeRow = row as any; + if (safeRow.operation === "delete") continue; + const entity_id = typeof safeRow.entity_id === "string" || typeof safeRow.entity_id === "number" + ? String(safeRow.entity_id).trim() + : ""; + const wiki_id = typeof safeRow.wiki_id === "string" || typeof safeRow.wiki_id === "number" + ? String(safeRow.wiki_id).trim() + : ""; + if (!entity_id || !wiki_id) continue; + const key = `${entity_id}::${wiki_id}`; + deduped.set(key, { + entity_id, + wiki_id, + operation: "reference", + }); + } + return Array.from(deduped.values()).sort((a, b) => { + const e = a.entity_id.localeCompare(b.entity_id); + if (e !== 0) return e; + return a.wiki_id.localeCompare(b.wiki_id); + }); +} + +export default function SubmissionDetailPage() { + return ( + + + + ); +} + +// Side Component to set the Zustand store state based on loaded data. +function StoreInitializer({ + project, + sessionSnapshot, +}: { + project: Project; + sessionSnapshot: EditorSnapshot; +}) { + const store = useEditorStoreApi(); + useEffect(() => { + if (!project || !sessionSnapshot) return; + const state = store.getState(); + state.setActiveSection(project); + state.setSelectedProjectId(project.id); + state.setBaselineSnapshot(sessionSnapshot); + state.setBaselineFeatureCollection(sessionSnapshot?.editor_feature_collection || EMPTY_FEATURE_COLLECTION); + state.setSnapshotEntityRows(sessionSnapshot?.entities || []); + state.setSnapshotWikis(sessionSnapshot?.wikis || []); + state.setSnapshotEntityWikiLinks(sessionSnapshot?.entity_wiki || []); + state.setBackgroundVisibility(loadBackgroundLayerVisibilityFromStorage()); + state.setIsBackgroundVisibilityReady(true); + + // Auto-detect the earliest year from geometries or entities to center the timeline. + let minYear: number | null = null; + if (sessionSnapshot?.geometries && sessionSnapshot.geometries.length > 0) { + for (const g of sessionSnapshot.geometries) { + if (g.time_start !== undefined && g.time_start !== null) { + const y = Number(g.time_start); + if (!Number.isNaN(y)) { + if (minYear === null || y < minYear) { + minYear = y; + } + } + } + } + } + if (minYear === null && sessionSnapshot?.entities && sessionSnapshot.entities.length > 0) { + for (const e of sessionSnapshot.entities) { + if (e.time_start !== undefined && e.time_start !== null) { + const y = Number(e.time_start); + if (!Number.isNaN(y)) { + if (minYear === null || y < minYear) { + minYear = y; + } + } + } + } + } + if (minYear !== null) { + state.setTimelineDraftYear(clampYearToFixedRange(minYear)); + } + }, [project, sessionSnapshot, store]); + + return null; +} + +function clampNumber(value: number, min: number, max: number): number { + if (value < min) return min; + if (value > max) return max; + return value; +} + +function SubmissionDetailPageContent() { + const params = useParams(); + const router = useRouter(); + const id = String(params.id || ""); + + const [submission, setSubmission] = useState(null); + const [project, setProject] = useState(null); + const [sessionSnapshot, setSessionSnapshot] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const [reviewNote, setReviewNote] = useState(""); + const [submitting, setSubmitting] = useState(false); + + // Retrieve Zustand state + const { + baselineFeatureCollection, + selectedFeatureIds, + setSelectedFeatureIds, + timelineDraftYear, + setTimelineDraftYear, + backgroundVisibility, + geometryVisibility, + snapshotEntityRows, + snapshotWikis, + snapshotEntityWikiLinks, + leftPanelWidth, + setLeftPanelWidth, + rightPanelWidth, + setRightPanelWidth, + timelineFilterEnabled, + setTimelineFilterEnabled, + } = useEditorStore(useShallow((state) => ({ + baselineFeatureCollection: state.baselineFeatureCollection, + selectedFeatureIds: state.selectedFeatureIds, + setSelectedFeatureIds: state.setSelectedFeatureIds, + timelineDraftYear: state.timelineDraftYear, + setTimelineDraftYear: state.setTimelineDraftYear, + backgroundVisibility: state.backgroundVisibility, + geometryVisibility: state.geometryVisibility, + snapshotEntityRows: state.snapshotEntityRows, + snapshotWikis: state.snapshotWikis, + snapshotEntityWikiLinks: state.snapshotEntityWikiLinks, + leftPanelWidth: state.leftPanelWidth, + setLeftPanelWidth: state.setLeftPanelWidth, + rightPanelWidth: state.rightPanelWidth, + setRightPanelWidth: state.setRightPanelWidth, + timelineFilterEnabled: state.timelineFilterEnabled, + setTimelineFilterEnabled: state.setTimelineFilterEnabled, + }))); + + // Fetch submission details and project commit snapshot + useEffect(() => { + async function loadData() { + try { + setLoading(true); + setError(null); + + const subRes = await apiGetSubmissionDetail(id); + if (!subRes || !subRes.status || !subRes.data) { + throw new Error("Không thể tải thông tin submission"); + } + const subData = subRes.data as SubmissionItem; + setSubmission(subData); + setReviewNote(subData.review_note || ""); + + const projectId = subData.project_id; + const commits = await fetchProjectCommits(projectId); + + const projRes = await requestJson(`${API_ENDPOINTS.projects}/${encodeURIComponent(projectId)}`); + setProject(projRes); + + const targetCommit = commits.find((c) => String(c.id) === String(subData.commit_id)); + if (!targetCommit) { + throw new Error(`Không tìm thấy commit #${subData.commit_id} của project`); + } + + const snapshot = normalizeEditorSnapshot(targetCommit.snapshot_json); + const session = snapshot ? toEditorSessionSnapshot(snapshot) : null; + setSessionSnapshot(session); + } catch (err: any) { + console.error("Error loading submission details:", err); + setError(err.message || "Lỗi hệ thống"); + } finally { + setLoading(false); + } + } + if (id) { + loadData(); + } + }, [id]); + + // Apply filters based on timeline year + const activeTimelineYear = timelineDraftYear; + const activeTimelineFilterEnabled = timelineFilterEnabled; + + const activeMapDraft = useMemo(() => { + const draft = baselineFeatureCollection || EMPTY_FEATURE_COLLECTION; + if (!activeTimelineFilterEnabled) return draft; + return { + type: "FeatureCollection", + features: draft.features.filter((f) => isFeatureVisibleAtYear(f, activeTimelineYear)), + }; + }, [baselineFeatureCollection, activeTimelineFilterEnabled, activeTimelineYear]); + + const mapLabelContextDraft = useMemo(() => { + return baselineFeatureCollection || EMPTY_FEATURE_COLLECTION; + }, [baselineFeatureCollection]); + + // Handle review (approve / reject) + const handleReview = async (status: "APPROVED" | "REJECTED") => { + if (!submission) return; + setSubmitting(true); + try { + const res = await updateSubmission(submission.id, { + status, + review_note: reviewNote, + }); + if (res?.status) { + toast.success("Cập nhật trạng thái submission thành công!"); + setSubmission((prev) => prev ? { ...prev, status, review_note: reviewNote } : null); + } else { + toast.error(res?.message || "Cập nhật thất bại"); + } + } catch (err: any) { + console.error(err); + toast.error("Lỗi khi cập nhật trạng thái submission"); + } finally { + setSubmitting(false); + } + }; + + // Right Sidebar Geometry Choices + const geometryChoices = useMemo(() => { + const draft = baselineFeatureCollection || EMPTY_FEATURE_COLLECTION; + const mapRenderGeometryIds = new Set( + activeMapDraft.features.map((feature) => String(feature.properties.id)) + ); + + const rows = draft.features + .filter((f) => f && f.properties && (typeof f.properties.id === "string" || typeof f.properties.id === "number")) + .map((f) => { + const id = String(f.properties.id); + const semantic = String(f.properties.type || getDefaultTypeIdForFeature(f) || "").trim(); + const label = semantic.length ? `${semantic} (${f.geometry.type})` : "Geometry"; + const timeStart = normalizeTimelineYearValue(f.properties.time_start); + const timeEnd = normalizeTimelineYearValue(f.properties.time_end); + const hasStart = timeStart !== null; + const hasEnd = timeEnd !== null; + const timeStatus: "missing" | "partial" | "complete" = + !hasStart && !hasEnd + ? "missing" + : !hasStart || !hasEnd + ? "partial" + : "complete"; + const isTimelineVisible = mapRenderGeometryIds.has(id); + const timelineStatus: "off" | "visible" | "filteredOut" = !activeTimelineFilterEnabled + ? "off" + : isTimelineVisible + ? "visible" + : "filteredOut"; + return { + id, + label, + time_start: timeStart, + time_end: timeEnd, + isTimelineVisible, + isOrphan: normalizeFeatureEntityIds(f).length === 0, + timeStatus, + timelineStatus, + isNew: false, + }; + }); + + rows.sort((a, b) => { + const na = String(a.label || a.id); + const nb = String(b.label || b.id); + return na.localeCompare(nb); + }); + return rows; + }, [baselineFeatureCollection, activeMapDraft, activeTimelineFilterEnabled]); + + // Selected features state resolution + const selectedFeatures = useMemo(() => { + const draft = baselineFeatureCollection || EMPTY_FEATURE_COLLECTION; + return selectedFeatureIds + .map((fid) => draft.features.find((f) => f.properties.id === fid) || null) + .filter((f): f is Feature => Boolean(f)); + }, [baselineFeatureCollection, selectedFeatureIds]); + + const selectedFeature = selectedFeatures[0] || null; + + const selectedGeometryChildIds = useMemo(() => { + if (!selectedFeature) return []; + return getDirectGeometryChildIds( + baselineFeatureCollection || EMPTY_FEATURE_COLLECTION, + String(selectedFeature.properties.id) + ); + }, [baselineFeatureCollection, selectedFeature]); + + const selectedGeometryTime = useMemo(() => { + if (!selectedFeature) return null; + const start = normalizeTimelineYearValue(selectedFeature.properties.time_start); + const end = normalizeTimelineYearValue(selectedFeature.properties.time_end); + return { time_start: start, time_end: end }; + }, [selectedFeature]); + + if (loading) { + return ( +
+
+
Đang tải dữ liệu bản đồ...
+ +
+ ); + } + + if (error || !submission || !project || !sessionSnapshot) { + return ( +
+ + + + + +
{error || "Không thể tìm thấy submission"}
+ +
+ ); + } + + return ( +
+ + + {/* Initialize store with project & snapshot data */} + + + {/* Left Sidebar showing submission details and review form */} +
+ {/* Header */} +
+ +
+
Chi tiết yêu cầu duyệt
+
#{submission.id.slice(0, 8)}
+
+
+ + {/* Info scroll area */} +
+ {/* Project Title */} +
+
Dự án
+
{submission.project_title}
+
{submission.project_description || "Không có mô tả dự án."}
+
+ + {/* Submitter */} +
+
Người gửi
+
+ {submission.user?.avatar_url ? ( + avatar + ) : ( +
+ {submission.user?.display_name?.charAt(0).toUpperCase() || "U"} +
+ )} +
+
{submission.user?.display_name || "N/A"}
+
{submission.user?.email || ""}
+
+
+
+ + {/* Meta grid */} +
+
+
Ngày tạo
+
{new Date(submission.created_at).toLocaleString("vi-VN")}
+
+
+
Trạng thái
+
+ + {submission.status} + +
+
+
+ + {submission.content && ( +
+
Nội dung ghi chú
+
+ {submission.content} +
+
+ )} + + {/* Review Forms */} +
+
Đánh giá của Admin
+ + {submission.status === "PENDING" ? ( +
+