From 3c7124992651ffad6c7b6c06a718c11e6ddecdcc Mon Sep 17 00:00:00 2001 From: ducanh Date: Thu, 14 May 2026 12:55:10 +0700 Subject: [PATCH 1/4] update wiki --- package-lock.json | 225 ++++++++++++++----- package.json | 1 + src/uhm/components/wiki/WikiSidebarPanel.tsx | 11 + 3 files changed, 176 insertions(+), 61 deletions(-) diff --git a/package-lock.json b/package-lock.json index 14b2041..614177b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "maplibre-gl": "^5.20.2", "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", @@ -102,7 +103,6 @@ "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", @@ -1955,7 +1955,6 @@ "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" } @@ -3027,7 +3026,6 @@ "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" @@ -3051,7 +3049,6 @@ "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" }, @@ -3228,7 +3225,6 @@ "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -3671,7 +3667,6 @@ "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.27.2.tgz", "integrity": "sha512-ABL1N6eoxzDzC1bYvkMbvyexHacszsKdVPYqhl5GwHLOvpZcv9VE9QaKwDILTyz5voCA0lGcAAXZp+qnXOk5lQ==", "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -3988,7 +3983,6 @@ "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.27.2.tgz", "integrity": "sha512-kaEg7BfiJPDQMKbjVIzEPO3wlcA+pZb2tlcK9gPrdDnEFaec2QTF1sXz2ak2IIb2curvnIrQ4yrfHgLlVA72wA==", "license": "MIT", - "peer": true, "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", @@ -4136,7 +4130,6 @@ "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -4154,7 +4147,6 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -4165,7 +4157,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -4240,7 +4231,6 @@ "integrity": "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/types": "8.58.0", @@ -4772,7 +4762,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4828,7 +4817,6 @@ "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", @@ -5240,7 +5228,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -5259,7 +5246,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", @@ -5291,7 +5277,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", @@ -5376,6 +5361,16 @@ "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", + "peer": true, + "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", @@ -5660,6 +5655,27 @@ } } }, + "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", + "peer": true, + "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", @@ -5681,7 +5697,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", @@ -5699,7 +5714,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", @@ -6103,7 +6117,6 @@ "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", @@ -6289,7 +6302,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -6530,10 +6542,18 @@ } }, "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" + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz", + "integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==", + "license": "MIT", + "peer": true + }, + "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", + "peer": true }, "node_modules/fast-deep-equal": { "version": "3.1.3", @@ -6782,7 +6802,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" @@ -6970,7 +6989,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" @@ -7122,6 +7140,23 @@ "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", + "peer": true, + "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", @@ -7274,7 +7309,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", @@ -7403,7 +7437,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", @@ -7589,8 +7622,7 @@ "version": "3.7.1", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/js-tokens": { "version": "4.0.0", @@ -8453,11 +8485,27 @@ "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", + "peer": true, + "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" @@ -8628,10 +8676,11 @@ } }, "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" + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz", + "integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==", + "license": "BSD-3-Clause", + "peer": true }, "node_modules/parent-module": { "version": "1.0.1", @@ -8771,7 +8820,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -8936,7 +8984,6 @@ "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz", "integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==", "license": "MIT", - "peer": true, "dependencies": { "orderedmap": "^2.0.0" } @@ -8966,7 +9013,6 @@ "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", "license": "MIT", - "peer": true, "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-transform": "^1.0.0", @@ -9015,7 +9061,6 @@ "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.8.tgz", "integrity": "sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA==", "license": "MIT", - "peer": true, "dependencies": { "prosemirror-model": "^1.20.0", "prosemirror-state": "^1.0.0", @@ -9084,18 +9129,39 @@ "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", + "peer": true, "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": { @@ -9103,7 +9169,6 @@ "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", @@ -9113,12 +9178,33 @@ "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", + "peer": true + }, + "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", + "peer": true, + "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", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -9180,7 +9266,6 @@ "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" }, @@ -9226,12 +9311,38 @@ "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", "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" @@ -9254,8 +9365,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -9313,7 +9423,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", @@ -9556,7 +9665,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", @@ -9574,7 +9682,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", @@ -10079,8 +10186,7 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tapable": { "version": "2.3.2", @@ -10136,7 +10242,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -10314,7 +10419,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10693,7 +10797,6 @@ "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 78467eb..fa08553 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "maplibre-gl": "^5.20.2", "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", diff --git a/src/uhm/components/wiki/WikiSidebarPanel.tsx b/src/uhm/components/wiki/WikiSidebarPanel.tsx index 31da701..64cbf84 100644 --- a/src/uhm/components/wiki/WikiSidebarPanel.tsx +++ b/src/uhm/components/wiki/WikiSidebarPanel.tsx @@ -109,6 +109,16 @@ export default function WikiSidebarPanel({ projectId, wikis, setWikis, autoOpen, const mod = await import("react-quill-new") as QuillModule; const Quill = mod?.Quill; if (!Quill) return; + + try { + const BlotFormatterModule = await import("quill-blot-formatter"); + const BlotFormatter = BlotFormatterModule.default; + // Only register if not already registered to avoid errors in dev/HMR + Quill.register("modules/blotFormatter", BlotFormatter, true); + } catch (err) { + console.error("Failed to load quill-blot-formatter", err); + } + const Link = Quill.import?.("formats/link"); if (!Link) return; @@ -537,6 +547,7 @@ export default function WikiSidebarPanel({ projectId, wikis, setWikis, autoOpen, }, }, }, + blotFormatter: {}, }; }, []); From f904f91a9cdd3a245b5ba2886c0fc499656d7a20 Mon Sep 17 00:00:00 2001 From: bokhonglo Date: Thu, 14 May 2026 16:07:16 +0700 Subject: [PATCH 2/4] update wiki page --- src/app/wiki/[slug]/wiki-by-slug-client.tsx | 456 ++++++++++++------- src/uhm/api/config.ts | 1 + src/uhm/api/wikis.ts | 11 + src/uhm/components/wiki/WikiSidebarPanel.tsx | 30 +- 4 files changed, 335 insertions(+), 163 deletions(-) diff --git a/src/app/wiki/[slug]/wiki-by-slug-client.tsx b/src/app/wiki/[slug]/wiki-by-slug-client.tsx index 3b59091..a551f6e 100644 --- a/src/app/wiki/[slug]/wiki-by-slug-client.tsx +++ b/src/app/wiki/[slug]/wiki-by-slug-client.tsx @@ -5,7 +5,7 @@ import Link from "next/link"; import "react-quill-new/dist/quill.snow.css"; import { ApiError } from "@/uhm/api/http"; -import { fetchWikiBySlug, type Wiki } from "@/uhm/api/wikis"; +import { fetchWikiBySlug, getContentByVersionWikiId, type Wiki } from "@/uhm/api/wikis"; type TocItem = { id: string; @@ -141,18 +141,33 @@ function rewriteHtmlAndBuildToc(inputHtml: string, wikiBaseUrl: string): { html: return { html: doc.body.innerHTML, toc }; } -function formatDate(value?: string | null): string { +function formatDate(value?: string | null, options?: Intl.DateTimeFormatOptions): string { const raw = String(value || "").trim(); if (!raw) return "-"; const d = new Date(raw); if (Number.isNaN(d.getTime())) return raw; - return d.toLocaleString(); + return d.toLocaleString( + "vi-VN", + options || { + hour: "2-digit", + minute: "2-digit", + day: "numeric", + month: "long", + year: "numeric", + } + ); } export default function WikiBySlugClient({ slug }: { slug: string }) { const [wiki, setWiki] = useState(null); const [status, setStatus] = useState<"idle" | "loading" | "error" | "ready">("idle"); const [error, setError] = useState(null); + + const [viewMode, setViewMode] = useState<"read" | "history" | "compare">("read"); + const [selectedVersionsForCompare, setSelectedVersionsForCompare] = useState>(new Set()); + const [comparisonData, setComparisonData] = useState<{ id: string; content: string; createdAt: string; title: string }[]>([]); + const [isComparing, setIsComparing] = useState(false); + const [renderHtml, setRenderHtml] = useState(""); const [toc, setToc] = useState([]); const [activeHeadingId, setActiveHeadingId] = useState(null); @@ -176,6 +191,21 @@ export default function WikiBySlugClient({ slug }: { slug: string }) { const hidePreviewTimerRef = useRef(null); const previewCacheRef = useRef>(new Map()); + const allVersions = useMemo(() => { + if (!wiki) return []; + const current = { + id: wiki.id, + created_at: wiki.updated_at, + content: wiki.content, + isCurrent: true, + }; + const history = (wiki.content_sample || []).map(s => ({ ...s, isCurrent: false })); + const uniqueHistory = history.filter(h => h.id !== current.id); + const combined = [current, ...uniqueHistory]; + return combined + .filter(v => v.id && v.created_at) + .sort((a, b) => new Date(b.created_at!).getTime() - new Date(a.created_at!).getTime()); + }, [wiki]); // Load wiki data by slug. useEffect(() => { const value = String(normalizedSlug || "").trim(); @@ -192,6 +222,18 @@ export default function WikiBySlugClient({ slug }: { slug: string }) { setError(null); try { const res = await fetchWikiBySlug(value); + let versionContent = res?.content; + try { + if (res?.content_sample?.[0]?.id) { + const contentResp = await getContentByVersionWikiId(res.content_sample[0].id); + if (contentResp?.data?.content) { + versionContent = contentResp.data.content; + } + } + } catch (err) { + console.error("Failed to fetch version content:", err); + } + if (disposed) return; if (!res) { setWiki(null); @@ -200,7 +242,7 @@ export default function WikiBySlugClient({ slug }: { slug: string }) { setToc([]); return; } - setWiki(res); + setWiki({ ...res, content: versionContent }); setStatus("ready"); } catch (err) { if (disposed) return; @@ -424,107 +466,210 @@ export default function WikiBySlugClient({ slug }: { slug: string }) { }; }, [renderHtml]); + const handleToggleVersionForCompare = (versionId: string) => { + setSelectedVersionsForCompare(prev => { + const next = new Set(prev); + if (next.has(versionId)) { + next.delete(versionId); + } else { + if (next.size >= 3) { + return prev; // Do not allow selecting more than 3 + } + next.add(versionId); + } + return next; + }); + }; + + const handleCompareVersions = async () => { + if (selectedVersionsForCompare.size < 1) { + alert("Vui lòng chọn ít nhất 1 phiên bản để so sánh."); + return; + } + setIsComparing(true); + setError(null); + try { + const versionsToFetch = Array.from(selectedVersionsForCompare); + const promises = versionsToFetch.map(async (versionId) => { + const sample = allVersions.find(s => s.id === versionId); + const versionInfo = { + id: versionId, + createdAt: sample?.created_at || 'Unknown date', + title: `Phiên bản lúc ${formatDate(sample?.created_at)}` + }; + if (sample?.isCurrent) { + return { ...versionInfo, content: sample.content || '' }; + } + const contentResp = await getContentByVersionWikiId(versionId); + return { ...versionInfo, content: contentResp?.data?.content || "" }; + }); + const results = await Promise.all(promises); + const processedResults = results.map(r => { + const { html } = rewriteHtmlAndBuildToc(normalizeWikiContentToHtml(r.content), `${window.location.origin}/wiki/`); + return { ...r, content: html }; + }); + setComparisonData(processedResults.sort((a,b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())); + setViewMode("compare"); + } catch (err) { + const msg = err instanceof ApiError ? err.message : err instanceof Error ? err.message : "Lỗi khi tải phiên bản để so sánh."; + setError(msg); + setViewMode("read"); + } finally { + setIsComparing(false); + } + }; + return ( -
-
-
-
-
Wiki
-

- {wiki?.title?.trim() || normalizedSlug || "Wiki"} -

-
- - Slug: {normalizedSlug || "-"} - - - ID: {wiki?.id || "-"} - - - Project:{" "} - {wiki?.project_id || "-"} - - - Created: {formatDate(wiki?.created_at)} - - - Updated: {formatDate(wiki?.updated_at)} - -
-
+
+
+
GeoHistory Wiki
+ Trang chủ +
-
- - Home - -
-
+
+ {status === "loading" &&
Đang tải...
} + {status === "error" &&
{error}
} + {status === "ready" && !wiki &&
Không tìm thấy wiki với slug: {normalizedSlug}
} - {status === "loading" ? ( -
-
-
-
- ) : status === "error" ? ( -
- {error || "Failed to load wiki."} -
- ) : wiki == null ? ( -
- Không tìm thấy wiki với slug: {normalizedSlug} -
- ) : ( -
- + )} +
-
-
-
-
-
-
+
+
+
+ + +
+ + {viewMode === 'read' && ( +
+ )} + + {viewMode === 'history' && ( +
+

Lịch sử phiên bản của "{wiki.title}"

+
+ +
+
+ + + + + + + + + + {allVersions.map((v) => { + const isChecked = selectedVersionsForCompare.has(v.id!); + const isDisabled = !isChecked && selectedVersionsForCompare.size >= 3; + return ( + + + + + + ); + })} + +
So sánhNgày cập nhậtGhi chú
+ handleToggleVersionForCompare(v.id!)} + checked={isChecked} + disabled={isDisabled} + className="h-4 w-4 disabled:cursor-not-allowed" + /> + {formatDate(v.created_at)}{v.isCurrent && (Phiên bản hiện tại)}
+
+
+ )} + + {viewMode === 'compare' && ( +
+
+

So sánh các phiên bản

+
+
= 3 ? 'xl:grid-cols-3' : ''} mx-auto px-4 sm:px-6`}> + {comparisonData.map(version => ( +
+

{version.title}

+
+
+ ))} +
+
+ )} +
+ + {viewMode !== 'compare' && ( + + )} +
+ )}
@@ -583,107 +728,94 @@ export default function WikiBySlugClient({ slug }: { slug: string }) { ) : null}
); diff --git a/src/uhm/api/config.ts b/src/uhm/api/config.ts index 06e9a29..90cc619 100644 --- a/src/uhm/api/config.ts +++ b/src/uhm/api/config.ts @@ -9,6 +9,7 @@ export const API_ENDPOINTS = { geometries: `${API_BASE_URL}/geometries`, entities: `${API_BASE_URL}/entities`, wikis: `${API_BASE_URL}/wikis`, + wikiContent: (id: string) => `${API_BASE_URL}/wikis/content/${id}`, // New API uses projects + commits + submissions (JWT-protected). authSignin: `${API_BASE_URL}/auth/signin`, authRefresh: `${API_BASE_URL}/auth/refresh`, diff --git a/src/uhm/api/wikis.ts b/src/uhm/api/wikis.ts index a3903dc..fc3f277 100644 --- a/src/uhm/api/wikis.ts +++ b/src/uhm/api/wikis.ts @@ -1,3 +1,4 @@ +import api from "@/config/config"; import { API_ENDPOINTS } from "@/uhm/api/config"; import { ApiError, requestJson } from "@/uhm/api/http"; @@ -10,6 +11,11 @@ export type Wiki = { is_deleted?: boolean; created_at?: string; updated_at?: string; + content_sample?:{ + created_at?: string; + content?: string; + id?: string; + }[]; }; export async function searchWikisByTitle(title: string, options?: { limit?: number; cursor?: string; entityId?: string }): Promise { @@ -60,3 +66,8 @@ export async function checkWikiSlugExists(slug: string): Promise { // Be conservative: unknown payload shape, treat as "exists" to prevent creating conflicting slugs. return true; } + +export const getContentByVersionWikiId = async (id: string) => { + const response = await api.get(API_ENDPOINTS.wikiContent(id)); + return response?.data; +}; \ No newline at end of file diff --git a/src/uhm/components/wiki/WikiSidebarPanel.tsx b/src/uhm/components/wiki/WikiSidebarPanel.tsx index 64cbf84..ddb7dac 100644 --- a/src/uhm/components/wiki/WikiSidebarPanel.tsx +++ b/src/uhm/components/wiki/WikiSidebarPanel.tsx @@ -28,6 +28,7 @@ type QuillLike = { type QuillModule = { Quill?: { import?: (path: string) => unknown; + register?: (pathOrModule: unknown, moduleOrOverwrite?: unknown, overwrite?: boolean) => void; }; }; type QuillLinkFormat = { @@ -114,11 +115,38 @@ export default function WikiSidebarPanel({ projectId, wikis, setWikis, autoOpen, const BlotFormatterModule = await import("quill-blot-formatter"); const BlotFormatter = BlotFormatterModule.default; // Only register if not already registered to avoid errors in dev/HMR - Quill.register("modules/blotFormatter", BlotFormatter, true); + Quill.register?.("modules/blotFormatter", BlotFormatter, true); } catch (err) { console.error("Failed to load quill-blot-formatter", err); } + const ImageFormat = Quill.import?.("formats/image") as any; + if (ImageFormat) { + class CustomImage extends ImageFormat { + static formats(domNode: Element) { + const formats = ImageFormat.formats(domNode) || {}; + if (domNode.hasAttribute("style")) formats.style = domNode.getAttribute("style"); + if (domNode.hasAttribute("width")) formats.width = domNode.getAttribute("width"); + if (domNode.hasAttribute("height")) formats.height = domNode.getAttribute("height"); + if (domNode.hasAttribute("class")) formats.class = domNode.getAttribute("class"); + return formats; + } + + format(name: string, value: string) { + if (["style", "width", "height", "class"].includes(name)) { + if (value) { + this.domNode.setAttribute(name, value); + } else { + this.domNode.removeAttribute(name); + } + } else { + super.format(name, value); + } + } + } + Quill.register?.(CustomImage, true); + } + const Link = Quill.import?.("formats/link"); if (!Link) return; From b54fdb987e8a0efefeccbdd783c978f9a06caa66 Mon Sep 17 00:00:00 2001 From: bokhonglo Date: Thu, 14 May 2026 16:12:26 +0700 Subject: [PATCH 3/4] restyle project/[id] page --- src/app/user/projects/[id]/page.tsx | 2 +- src/app/user/projects/page.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/user/projects/[id]/page.tsx b/src/app/user/projects/[id]/page.tsx index 4728c2e..38fcb3a 100644 --- a/src/app/user/projects/[id]/page.tsx +++ b/src/app/user/projects/[id]/page.tsx @@ -257,7 +257,7 @@ export default function ProjectDetailsPage() {
-
+
{[ { id: "overview", diff --git a/src/app/user/projects/page.tsx b/src/app/user/projects/page.tsx index 93901eb..5063532 100644 --- a/src/app/user/projects/page.tsx +++ b/src/app/user/projects/page.tsx @@ -329,7 +329,7 @@ export default function ProjectsPage() {
-
+
); }; - console.log(projects); - const importLabel = useMemo(() => { if (!importSnapshotName) return "Chưa chọn JSON snapshot"; return `JSON: ${importSnapshotName}`; @@ -266,137 +268,134 @@ export default function ProjectsPage() { {!isLoading && sortedProjects.length > 0 ? (
-
-
- -
- Sắp xếp: - - +
+
+
+ +
+
Trạng thái
+
Thành viên
+
+
Thao tác
{sortedProjects.map((project: any) => (
-
-
router.push(`/user/projects/${project.id}`)} - className="flex items-center gap-2 mb-2 cursor-pointer hover:underline" - > -
+
+
+

router.push(`/user/projects/${project.id}`)} + className="font-semibold text-blue-600 dark:text-[#58a6ff] truncate cursor-pointer hover:underline" + > + {project.title} +

+ +
+
+
{project.user?.avatar_url ? ( -
- avatar -
+ avatar ) : ( -
- +
+ {project.user?.display_name?.charAt(0)?.toUpperCase() || "U"}
)} + {project.user?.display_name || "Unknown"}
- -
- - {project.user?.display_name || "Unknown"} - -
- - / - -

- {project.title} -

- -
- {getStatusBadge(project.project_status)} -
-
- -
- {formatDate(project.updated_at)}
- -
- - - - + +
+ {getStatusBadge(project.project_status)} +
+ +
{project.members && project.members.length > 0 ? ( <> {project.members.slice(0, 4).map((m: any, index: number) => m.avatar_url ? ( - {m.display_name} + {m.display_name} ) : ( -
- - {m.display_name?.charAt(0)?.toUpperCase() || "U"} - +
+ {m.display_name?.charAt(0)?.toUpperCase() || "U"}
) )} - {project.members.length > 4 && ( -
- - +{project.members.length - 4} - +
+ +{project.members.length - 4}
)} ) : ( - + )}
+ +
+ {formatDate(project.updated_at)} +
+ +
+
+ + + Editor + +
+ +
+ + + Export JSON + +
+ +
+ + + Wiki Editor + +
+
))}
@@ -414,7 +413,6 @@ export default function ProjectsPage() {
- {/* Modal Tạo Dự án */}

Tạo dự án mới

@@ -484,7 +482,6 @@ export default function ProjectsPage() { disabled={isSubmitting} className="bg-gray-900 hover:bg-gray-800 text-white" onClick={handleCreateProjectWithJson} - // title="Tạo dự án và tạo commit đầu tiên từ JSON snapshot" > Tạo với JSON diff --git a/src/components/ui/button/Button.tsx b/src/components/ui/button/Button.tsx index 9d9593a..e09fb78 100644 --- a/src/components/ui/button/Button.tsx +++ b/src/components/ui/button/Button.tsx @@ -5,6 +5,7 @@ type ButtonProps = React.ButtonHTMLAttributes & { variant?: "primary" | "outline"; // Button variant startIcon?: ReactNode; // Icon before the text endIcon?: ReactNode; // Icon after the text + title?: string; // Title text }; const Button: React.FC = ({ @@ -16,6 +17,7 @@ const Button: React.FC = ({ className = "", disabled = false, type = "button", + title, ...rest }) => { // Size Classes @@ -39,6 +41,7 @@ const Button: React.FC = ({ } ${variantClasses[variant]} ${ disabled ? "cursor-not-allowed opacity-50" : "" }`} + title={title} disabled={disabled} type={type} {...rest}