smaller changes
This commit is contained in:
parent
9233b48758
commit
a18a57114d
320
package-lock.json
generated
320
package-lock.json
generated
@ -9,7 +9,7 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@marsidev/react-turnstile": "^1.5.4",
|
"@marsidev/react-turnstile": "^1.5.4",
|
||||||
"@vnedyalk0v/react19-simple-maps": "^2.0.9",
|
"@vnedyalk0v/react19-simple-maps": "^2.0.10",
|
||||||
"axios": "^1.19.0",
|
"axios": "^1.19.0",
|
||||||
"country-list": "^2.4.1",
|
"country-list": "^2.4.1",
|
||||||
"d3-geo": "^3.1.1",
|
"d3-geo": "^3.1.1",
|
||||||
@ -32,16 +32,16 @@
|
|||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@types/country-list": "^2.1.4",
|
"@types/country-list": "^2.1.4",
|
||||||
"@types/node": "^26.1.2",
|
"@types/node": "^26.1.2",
|
||||||
"@types/react": "^19.2.17",
|
"@types/react": "^19.2.18",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.4",
|
||||||
"@vitejs/plugin-react": "^6.0.4",
|
"@vitejs/plugin-react": "^6.0.5",
|
||||||
"eslint": "^10.8.0",
|
"eslint": "^10.8.0",
|
||||||
"eslint-plugin-react-hooks": "^7.1.1",
|
"eslint-plugin-react-hooks": "^7.1.1",
|
||||||
"eslint-plugin-react-refresh": "^0.5.3",
|
"eslint-plugin-react-refresh": "^0.5.3",
|
||||||
"globals": "^17.8.0",
|
"globals": "^17.8.0",
|
||||||
"typescript": "^7.0.2",
|
"typescript": "^7.0.2",
|
||||||
"typescript-eslint": "^8.65.0",
|
"typescript-eslint": "^8.65.0",
|
||||||
"vite": "^8.1.5"
|
"vite": "^8.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
@ -294,21 +294,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/core": {
|
"node_modules/@emnapi/core": {
|
||||||
"version": "1.11.1",
|
"version": "2.0.0-alpha.3",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-2.0.0-alpha.3.tgz",
|
||||||
"integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
|
"integrity": "sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emnapi/wasi-threads": "1.2.2",
|
"@emnapi/wasi-threads": "2.0.1",
|
||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/runtime": {
|
"node_modules/@emnapi/runtime": {
|
||||||
"version": "1.11.1",
|
"version": "2.0.0-alpha.3",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-2.0.0-alpha.3.tgz",
|
||||||
"integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
|
"integrity": "sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@ -317,9 +317,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/wasi-threads": {
|
"node_modules/@emnapi/wasi-threads": {
|
||||||
"version": "1.2.2",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-2.0.1.tgz",
|
||||||
"integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
|
"integrity": "sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@ -582,28 +582,31 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@napi-rs/wasm-runtime": {
|
"node_modules/@napi-rs/wasm-runtime": {
|
||||||
"version": "1.1.6",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.2.tgz",
|
||||||
"integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==",
|
"integrity": "sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tybys/wasm-util": "^0.10.3"
|
"@tybys/wasm-util": "^0.10.3"
|
||||||
},
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || ^22.13.0 || >=23.5.0"
|
||||||
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@emnapi/core": "^1.7.1",
|
"@emnapi/core": "^1.7.1 || ^2.0.0-alpha.3",
|
||||||
"@emnapi/runtime": "^1.7.1"
|
"@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@oxc-project/types": {
|
"node_modules/@oxc-project/types": {
|
||||||
"version": "0.139.0",
|
"version": "0.142.0",
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz",
|
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.142.0.tgz",
|
||||||
"integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==",
|
"integrity": "sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
@ -637,9 +640,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-android-arm64": {
|
"node_modules/@rolldown/binding-android-arm64": {
|
||||||
"version": "1.1.5",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.1.tgz",
|
||||||
"integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==",
|
"integrity": "sha512-02hOeOSryYxVrOIphmLAsqnCJWxwlzFk+pEt/N/i6OgT3lShHO7xGCU5cpgchRDHboAEbSjzgGh+O/u1GswQmA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@ -654,9 +657,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||||
"version": "1.1.5",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.1.tgz",
|
||||||
"integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==",
|
"integrity": "sha512-fMsTOnN0OjFm3CyppWPitKnc8UlliVARUULW6cfU6AIqjdtgmSFWSk9vecHzZduv/yMWIHDlRhM1e8Iff9uAfA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@ -671,9 +674,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-darwin-x64": {
|
"node_modules/@rolldown/binding-darwin-x64": {
|
||||||
"version": "1.1.5",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.1.tgz",
|
||||||
"integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==",
|
"integrity": "sha512-1wjKdz/XLGKHaTNHjQveQ/B23TKx4ItAqm1JbyVuvNPc4Ze0Fb48s49TAd/2zcplPl8okE/UbTgmlVfwT7eFeQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -688,9 +691,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||||
"version": "1.1.5",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.1.tgz",
|
||||||
"integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==",
|
"integrity": "sha512-Fa0jHR07E7YBN4vOEsbVf2briYNsuOowfLJaXULZM0ldMlaCaj2LJgLMbMe4iacRyZmvR8efFhgR9wKuGclQUg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -705,9 +708,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||||
"version": "1.1.5",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.1.tgz",
|
||||||
"integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==",
|
"integrity": "sha512-pzkgu1SSHGgRRyRZ4fbmSgmajbVt+epaLP99NDjFft69v/ypfTi6swBMiVdh2EkQ0OSnHE1lZDM7DRGkyAzUpA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@ -722,9 +725,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||||
"version": "1.1.5",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.1.tgz",
|
||||||
"integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==",
|
"integrity": "sha512-QI5SEDY8cbiYWHx0VO4vIc3UlS6a32vXHjU8Qy/17adEmZIPuByJg13UEvo9c/UCiUkdcVWY83C+b+JrwnNyUg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@ -739,9 +742,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||||
"version": "1.1.5",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.1.tgz",
|
||||||
"integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==",
|
"integrity": "sha512-Sm41FyCeXqmYcERoYOCbGIL5hNfd8w9LQ7Y61Bev48HkcjaJqV/iiVOaiDxjVTRMS+QKrZmD8cfPt4uMVnvM+A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@ -756,9 +759,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||||
"version": "1.1.5",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.1.tgz",
|
||||||
"integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==",
|
"integrity": "sha512-2x+WhXTGl9yJYPbltW/BSEPTVz9OIWQyER4N+gJEDWkkn904eRcBzELqh/Hf7K0w/ubGbKNMv0ZC+94QK/IFEg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
@ -773,9 +776,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||||
"version": "1.1.5",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.1.tgz",
|
||||||
"integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==",
|
"integrity": "sha512-eEjmQpuRQayHPWWnywaWHkFT3ToPbP3RYy42VVd/B9aBGDA+Ol25EIWHxKQST3IiWJjikCWUF7KtbfqwZrzVwQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
@ -790,9 +793,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||||
"version": "1.1.5",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.1.tgz",
|
||||||
"integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==",
|
"integrity": "sha512-/Orga1fZYkLc/56jBICcHrKchl8Z2UKdDSr3LG9ToWO1lQ6a4Livk9Xz+9WN91zsz5QR3XQz2NNoSDEvP6qadw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -807,9 +810,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||||
"version": "1.1.5",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.1.tgz",
|
||||||
"integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==",
|
"integrity": "sha512-xxBJRL+0q0Kce7orznGWLuylHDY65vuARXZRpX+hPdv+DqK2c3NlCsVA98tlWzWNEE7yPqA/1NQ5nnCrj49Y5A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -824,9 +827,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||||
"version": "1.1.5",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.1.tgz",
|
||||||
"integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==",
|
"integrity": "sha512-M6AdXIXw3s+/8XpKMzdGDEXGS1S7kwUsy+rcTIUIOx5Ge4nXKCtAFHFV9YKkXvGcC5WMoTjAteLzlsQROVI0Yw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@ -841,28 +844,25 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-wasm32-wasi": {
|
"node_modules/@rolldown/binding-wasm32-wasi": {
|
||||||
"version": "1.1.5",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.2.1.tgz",
|
||||||
"integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==",
|
"integrity": "sha512-/TX0SoRGojHzSAHpfVBbavRVSazg5U3h3Y3VXfcc0cdugq6kxdqw8LPGFiPr+/7gE/60zRcsOY2Vi9b9eT0jww==",
|
||||||
"cpu": [
|
|
||||||
"wasm32"
|
|
||||||
],
|
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emnapi/core": "1.11.1",
|
"@emnapi/core": "2.0.0-alpha.3",
|
||||||
"@emnapi/runtime": "1.11.1",
|
"@emnapi/runtime": "2.0.0-alpha.3",
|
||||||
"@napi-rs/wasm-runtime": "^1.1.6"
|
"@napi-rs/wasm-runtime": "^1.2.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || ^22.13.0 || >=23.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||||
"version": "1.1.5",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.1.tgz",
|
||||||
"integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==",
|
"integrity": "sha512-EvRrivJieyHG+AO9lleZWgq+g0+S7oV2C51yuqlcyU/R9net+sI4Pj0F+lUoP2bEr6TWX3SqFaaS0SzfLxSzkw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@ -877,9 +877,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||||
"version": "1.1.5",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.1.tgz",
|
||||||
"integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==",
|
"integrity": "sha512-Z4eCmn5QJ/5+azF9knpLWKfVd9aidn0mAe9TpJgvBLId9Ax3t0+JVxBmT25Bv7NBbVW1TZyKjQjQReouMeH5UQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -1066,9 +1066,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "19.2.17",
|
"version": "19.2.18",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz",
|
||||||
"integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
|
"integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -1076,9 +1076,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/react-dom": {
|
"node_modules/@types/react-dom": {
|
||||||
"version": "19.2.3",
|
"version": "19.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz",
|
||||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
"integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
@ -1687,9 +1687,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/@vitejs/plugin-react": {
|
"node_modules/@vitejs/plugin-react": {
|
||||||
"version": "6.0.4",
|
"version": "6.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.5.tgz",
|
||||||
"integrity": "sha512-XcCQz0TBpBgljhj0gMuuDj49i6Ytqh5q1osT/Gp5uAVJUCTWxyskk/l1jwYYiu2xcNHHipdMz40EGfM1VdamVg==",
|
"integrity": "sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -1713,9 +1713,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vnedyalk0v/react19-simple-maps": {
|
"node_modules/@vnedyalk0v/react19-simple-maps": {
|
||||||
"version": "2.0.9",
|
"version": "2.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/@vnedyalk0v/react19-simple-maps/-/react19-simple-maps-2.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/@vnedyalk0v/react19-simple-maps/-/react19-simple-maps-2.0.10.tgz",
|
||||||
"integrity": "sha512-QMV6QkRcAVulhgA4vEVE68tvXk7xVn5MAi5gCTSfWhWFd4YRwnGaRLA/m4UEmhTRuXe4dabGVIVKFSMfHBy77Q==",
|
"integrity": "sha512-tw8PYah/5c1UiwqhtB0k40p+jlYRW9QjdZZQ37CKONXErIZFHl0Sn8Ho03HNP/mXQs0YrogrBIeolln5mfbIsA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"d3-color": "^3.1.0",
|
"d3-color": "^3.1.0",
|
||||||
@ -3252,9 +3252,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss": {
|
"node_modules/lightningcss": {
|
||||||
"version": "1.32.0",
|
"version": "1.33.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
|
||||||
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
|
"integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -3268,23 +3268,23 @@
|
|||||||
"url": "https://opencollective.com/parcel"
|
"url": "https://opencollective.com/parcel"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"lightningcss-android-arm64": "1.32.0",
|
"lightningcss-android-arm64": "1.33.0",
|
||||||
"lightningcss-darwin-arm64": "1.32.0",
|
"lightningcss-darwin-arm64": "1.33.0",
|
||||||
"lightningcss-darwin-x64": "1.32.0",
|
"lightningcss-darwin-x64": "1.33.0",
|
||||||
"lightningcss-freebsd-x64": "1.32.0",
|
"lightningcss-freebsd-x64": "1.33.0",
|
||||||
"lightningcss-linux-arm-gnueabihf": "1.32.0",
|
"lightningcss-linux-arm-gnueabihf": "1.33.0",
|
||||||
"lightningcss-linux-arm64-gnu": "1.32.0",
|
"lightningcss-linux-arm64-gnu": "1.33.0",
|
||||||
"lightningcss-linux-arm64-musl": "1.32.0",
|
"lightningcss-linux-arm64-musl": "1.33.0",
|
||||||
"lightningcss-linux-x64-gnu": "1.32.0",
|
"lightningcss-linux-x64-gnu": "1.33.0",
|
||||||
"lightningcss-linux-x64-musl": "1.32.0",
|
"lightningcss-linux-x64-musl": "1.33.0",
|
||||||
"lightningcss-win32-arm64-msvc": "1.32.0",
|
"lightningcss-win32-arm64-msvc": "1.33.0",
|
||||||
"lightningcss-win32-x64-msvc": "1.32.0"
|
"lightningcss-win32-x64-msvc": "1.33.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-android-arm64": {
|
"node_modules/lightningcss-android-arm64": {
|
||||||
"version": "1.32.0",
|
"version": "1.33.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
|
||||||
"integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
|
"integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@ -3303,9 +3303,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-darwin-arm64": {
|
"node_modules/lightningcss-darwin-arm64": {
|
||||||
"version": "1.32.0",
|
"version": "1.33.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
|
||||||
"integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
|
"integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@ -3324,9 +3324,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-darwin-x64": {
|
"node_modules/lightningcss-darwin-x64": {
|
||||||
"version": "1.32.0",
|
"version": "1.33.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
|
||||||
"integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
|
"integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -3345,9 +3345,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-freebsd-x64": {
|
"node_modules/lightningcss-freebsd-x64": {
|
||||||
"version": "1.32.0",
|
"version": "1.33.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
|
||||||
"integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
|
"integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -3366,9 +3366,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
||||||
"version": "1.32.0",
|
"version": "1.33.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
|
||||||
"integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
|
"integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@ -3387,9 +3387,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-arm64-gnu": {
|
"node_modules/lightningcss-linux-arm64-gnu": {
|
||||||
"version": "1.32.0",
|
"version": "1.33.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
|
||||||
"integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
|
"integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@ -3408,9 +3408,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-arm64-musl": {
|
"node_modules/lightningcss-linux-arm64-musl": {
|
||||||
"version": "1.32.0",
|
"version": "1.33.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",
|
||||||
"integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
|
"integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@ -3429,9 +3429,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-x64-gnu": {
|
"node_modules/lightningcss-linux-x64-gnu": {
|
||||||
"version": "1.32.0",
|
"version": "1.33.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",
|
||||||
"integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
|
"integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -3450,9 +3450,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-x64-musl": {
|
"node_modules/lightningcss-linux-x64-musl": {
|
||||||
"version": "1.32.0",
|
"version": "1.33.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",
|
||||||
"integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
|
"integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -3471,9 +3471,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-win32-arm64-msvc": {
|
"node_modules/lightningcss-win32-arm64-msvc": {
|
||||||
"version": "1.32.0",
|
"version": "1.33.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",
|
||||||
"integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
|
"integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@ -3492,9 +3492,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-win32-x64-msvc": {
|
"node_modules/lightningcss-win32-x64-msvc": {
|
||||||
"version": "1.32.0",
|
"version": "1.33.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
|
||||||
"integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
|
"integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -4960,13 +4960,13 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/rolldown": {
|
"node_modules/rolldown": {
|
||||||
"version": "1.1.5",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.1.tgz",
|
||||||
"integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==",
|
"integrity": "sha512-4FKJhg8d3OiyQOA6Q1Q0hoFFpW9/OoX+VsHzpECsdsIZoOArrAK90gl59YK/Z+gnDel45bgJZK03ozH/9bCqEw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oxc-project/types": "=0.139.0",
|
"@oxc-project/types": "=0.142.0",
|
||||||
"@rolldown/pluginutils": "^1.0.0"
|
"@rolldown/pluginutils": "^1.0.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@ -4976,21 +4976,21 @@
|
|||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@rolldown/binding-android-arm64": "1.1.5",
|
"@rolldown/binding-android-arm64": "1.2.1",
|
||||||
"@rolldown/binding-darwin-arm64": "1.1.5",
|
"@rolldown/binding-darwin-arm64": "1.2.1",
|
||||||
"@rolldown/binding-darwin-x64": "1.1.5",
|
"@rolldown/binding-darwin-x64": "1.2.1",
|
||||||
"@rolldown/binding-freebsd-x64": "1.1.5",
|
"@rolldown/binding-freebsd-x64": "1.2.1",
|
||||||
"@rolldown/binding-linux-arm-gnueabihf": "1.1.5",
|
"@rolldown/binding-linux-arm-gnueabihf": "1.2.1",
|
||||||
"@rolldown/binding-linux-arm64-gnu": "1.1.5",
|
"@rolldown/binding-linux-arm64-gnu": "1.2.1",
|
||||||
"@rolldown/binding-linux-arm64-musl": "1.1.5",
|
"@rolldown/binding-linux-arm64-musl": "1.2.1",
|
||||||
"@rolldown/binding-linux-ppc64-gnu": "1.1.5",
|
"@rolldown/binding-linux-ppc64-gnu": "1.2.1",
|
||||||
"@rolldown/binding-linux-s390x-gnu": "1.1.5",
|
"@rolldown/binding-linux-s390x-gnu": "1.2.1",
|
||||||
"@rolldown/binding-linux-x64-gnu": "1.1.5",
|
"@rolldown/binding-linux-x64-gnu": "1.2.1",
|
||||||
"@rolldown/binding-linux-x64-musl": "1.1.5",
|
"@rolldown/binding-linux-x64-musl": "1.2.1",
|
||||||
"@rolldown/binding-openharmony-arm64": "1.1.5",
|
"@rolldown/binding-openharmony-arm64": "1.2.1",
|
||||||
"@rolldown/binding-wasm32-wasi": "1.1.5",
|
"@rolldown/binding-wasm32-wasi": "1.2.1",
|
||||||
"@rolldown/binding-win32-arm64-msvc": "1.1.5",
|
"@rolldown/binding-win32-arm64-msvc": "1.2.1",
|
||||||
"@rolldown/binding-win32-x64-msvc": "1.1.5"
|
"@rolldown/binding-win32-x64-msvc": "1.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/scheduler": {
|
"node_modules/scheduler": {
|
||||||
@ -5435,16 +5435,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "8.1.5",
|
"version": "8.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-8.2.0.tgz",
|
||||||
"integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==",
|
"integrity": "sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lightningcss": "^1.32.0",
|
"lightningcss": "^1.33.0",
|
||||||
"picomatch": "^4.0.5",
|
"picomatch": "^4.0.5",
|
||||||
"postcss": "^8.5.17",
|
"postcss": "^8.5.23",
|
||||||
"rolldown": "~1.1.5",
|
"rolldown": "~1.2.0",
|
||||||
"tinyglobby": "^0.2.17"
|
"tinyglobby": "^0.2.17"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@ -5461,7 +5461,7 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/node": "^20.19.0 || >=22.12.0",
|
"@types/node": "^20.19.0 || >=22.12.0",
|
||||||
"@vitejs/devtools": "^0.3.0",
|
"@vitejs/devtools": "^0.4.0",
|
||||||
"esbuild": "^0.27.0 || ^0.28.0",
|
"esbuild": "^0.27.0 || ^0.28.0",
|
||||||
"jiti": ">=1.21.0",
|
"jiti": ">=1.21.0",
|
||||||
"less": "^4.0.0",
|
"less": "^4.0.0",
|
||||||
|
|||||||
12
package.json
12
package.json
@ -12,7 +12,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@marsidev/react-turnstile": "^1.5.4",
|
"@marsidev/react-turnstile": "^1.5.4",
|
||||||
"@vnedyalk0v/react19-simple-maps": "^2.0.9",
|
"@vnedyalk0v/react19-simple-maps": "^2.0.10",
|
||||||
"axios": "^1.19.0",
|
"axios": "^1.19.0",
|
||||||
"country-list": "^2.4.1",
|
"country-list": "^2.4.1",
|
||||||
"d3-geo": "^3.1.1",
|
"d3-geo": "^3.1.1",
|
||||||
@ -35,18 +35,18 @@
|
|||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@types/country-list": "^2.1.4",
|
"@types/country-list": "^2.1.4",
|
||||||
"@types/node": "^26.1.2",
|
"@types/node": "^26.1.2",
|
||||||
"@types/react": "^19.2.17",
|
"@types/react": "^19.2.18",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.4",
|
||||||
"@vitejs/plugin-react": "^6.0.4",
|
"@vitejs/plugin-react": "^6.0.5",
|
||||||
"eslint": "^10.8.0",
|
"eslint": "^10.8.0",
|
||||||
"eslint-plugin-react-hooks": "^7.1.1",
|
"eslint-plugin-react-hooks": "^7.1.1",
|
||||||
"eslint-plugin-react-refresh": "^0.5.3",
|
"eslint-plugin-react-refresh": "^0.5.3",
|
||||||
"globals": "^17.8.0",
|
"globals": "^17.8.0",
|
||||||
"typescript": "^7.0.2",
|
"typescript": "^7.0.2",
|
||||||
"typescript-eslint": "^8.65.0",
|
"typescript-eslint": "^8.65.0",
|
||||||
"vite": "^8.1.5"
|
"vite": "^8.2.0"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"vite": "^8.1.5"
|
"vite": "^8.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
59
src/App.css
59
src/App.css
@ -64,12 +64,10 @@ h3 {
|
|||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title span {
|
.section-title span {
|
||||||
color: var(--primary);
|
color: inherit;
|
||||||
border-bottom: 2px solid var(--primary);
|
|
||||||
padding-bottom: 4px;
|
padding-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -194,35 +192,48 @@ h3 {
|
|||||||
text-decoration: none !important;
|
text-decoration: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-search {
|
.nav-search-center {
|
||||||
flex-grow: 1;
|
flex: 1;
|
||||||
margin: 0 2rem;
|
display: flex;
|
||||||
max-width: 400px;
|
justify-content: center;
|
||||||
|
margin: 0 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-search input {
|
.nav-search-input-wrap {
|
||||||
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px 15px;
|
max-width: 440px;
|
||||||
border-radius: 6px;
|
display: flex;
|
||||||
border: 1px solid var(--border);
|
align-items: center;
|
||||||
background: var(--surface);
|
|
||||||
color: var(--text);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
outline: none;
|
|
||||||
transition: border-color 0.2s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-search input::placeholder {
|
.nav-search-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 14px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-search-input-wrap input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 16px 8px 40px;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-search-input-wrap input::placeholder {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-search input:focus {
|
.nav-search-input-wrap input:focus {
|
||||||
border-color: var(--primary);
|
border-color: var(--primary);
|
||||||
}
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
box-shadow: 0 0 12px rgba(249, 115, 22, 0.25);
|
||||||
.nav-search-right {
|
|
||||||
margin-right: 1rem;
|
|
||||||
width: 250px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-search-right input {
|
.nav-search-right input {
|
||||||
@ -296,7 +307,7 @@ h3 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.plugin-sidebar {
|
.plugin-sidebar {
|
||||||
order: -1;
|
order: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
31
src/App.tsx
31
src/App.tsx
@ -1,6 +1,7 @@
|
|||||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||||
import { BrowserRouter as Router, Routes, Route, Link, NavLink, useNavigate, Outlet, Navigate, useLocation } from 'react-router-dom';
|
import { BrowserRouter as Router, Routes, Route, Link, NavLink, useNavigate, Outlet, Navigate, useLocation } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Search } from 'lucide-react';
|
||||||
import Home from './pages/Home';
|
import Home from './pages/Home';
|
||||||
import PluginDetail from './pages/PluginDetail';
|
import PluginDetail from './pages/PluginDetail';
|
||||||
import LoginPage from './pages/auth/LoginPage';
|
import LoginPage from './pages/auth/LoginPage';
|
||||||
@ -171,9 +172,14 @@ import './pages/dashboard/Dashboard.css';
|
|||||||
const DashboardLayout = () => {
|
const DashboardLayout = () => {
|
||||||
return (
|
return (
|
||||||
<div className="dashboard-container">
|
<div className="dashboard-container">
|
||||||
<nav className="dashboard-nav">
|
<header className="dashboard-app-header">
|
||||||
<NavLink to="/dashboard/plugins">My Plugins</NavLink>
|
<div className="dashboard-app-header-inner">
|
||||||
</nav>
|
<Link to="/dashboard/plugins" className="dashboard-app-brand">
|
||||||
|
<img src="/icon.png" alt="Market Logo" className="dashboard-app-logo" />
|
||||||
|
<span className="dashboard-app-title">DEVELOPER <span>STUDIO</span></span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
<div className="dashboard-content">
|
<div className="dashboard-content">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
@ -227,8 +233,10 @@ const Navbar = ({ user }: any) => {
|
|||||||
{user && (user.role === 'admin' || user.role === 'moderator') && <NavLink to="/staff" onClick={() => setIsMenuOpen(false)}>Staff</NavLink>}
|
{user && (user.role === 'admin' || user.role === 'moderator') && <NavLink to="/staff" onClick={() => setIsMenuOpen(false)}>Staff</NavLink>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!user && (
|
{/* ── Center Search Bar ── */}
|
||||||
<div className="nav-search">
|
<div className="nav-search-center">
|
||||||
|
<div className="nav-search-input-wrap">
|
||||||
|
<Search size={16} className="nav-search-icon" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={t('search')}
|
placeholder={t('search')}
|
||||||
@ -237,22 +245,11 @@ const Navbar = ({ user }: any) => {
|
|||||||
onKeyDown={handleSearch}
|
onKeyDown={handleSearch}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
<div className="nav-actions">
|
<div className="nav-actions">
|
||||||
{user ? (
|
{user ? (
|
||||||
<>
|
|
||||||
<div className="nav-search-right">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder={t('search')}
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
onKeyDown={handleSearch}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<NavLink to="/settings" className="nav-user-link" onClick={() => setIsMenuOpen(false)}>{user.username}</NavLink>
|
<NavLink to="/settings" className="nav-user-link" onClick={() => setIsMenuOpen(false)}>{user.username}</NavLink>
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Link to="/login" className="btn btn-secondary" onClick={() => setIsMenuOpen(false)}>{t('login')}</Link>
|
<Link to="/login" className="btn btn-secondary" onClick={() => setIsMenuOpen(false)}>{t('login')}</Link>
|
||||||
|
|||||||
@ -31,7 +31,7 @@ const getEffectivePrice = (plugin: any): { display: string; isSale: boolean; ori
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const PluginCard = ({ plugin }: { plugin: any }) => {
|
const PluginCard = ({ plugin, hideDescription = false }: { plugin: any; hideDescription?: boolean }) => {
|
||||||
const { i18n } = useTranslation();
|
const { i18n } = useTranslation();
|
||||||
const [currentIndex, setCurrentIndex] = useState(0);
|
const [currentIndex, setCurrentIndex] = useState(0);
|
||||||
const [isHovering, setIsHovering] = useState(false);
|
const [isHovering, setIsHovering] = useState(false);
|
||||||
@ -129,7 +129,7 @@ const PluginCard = ({ plugin }: { plugin: any }) => {
|
|||||||
{plugin.is_early_access && <span className="pcv2-badge-ea">Early Access</span>}
|
{plugin.is_early_access && <span className="pcv2-badge-ea">Early Access</span>}
|
||||||
</p>
|
</p>
|
||||||
<p className="pcv2-dev">by {plugin.dev_name}</p>
|
<p className="pcv2-dev">by {plugin.dev_name}</p>
|
||||||
<p className="pcv2-desc">{desc}</p>
|
{!hideDescription && <p className="pcv2-desc">{desc}</p>}
|
||||||
|
|
||||||
<div className="pcv2-footer">
|
<div className="pcv2-footer">
|
||||||
<span className="pcv2-stat">
|
<span className="pcv2-stat">
|
||||||
|
|||||||
@ -219,7 +219,7 @@ const Home = () => {
|
|||||||
<div className="home-plugin-grid">
|
<div className="home-plugin-grid">
|
||||||
{popular.length > 0 ? (
|
{popular.length > 0 ? (
|
||||||
popular.map((plugin) => (
|
popular.map((plugin) => (
|
||||||
<PluginCard key={`pop-${plugin.id}`} plugin={plugin} />
|
<PluginCard key={`pop-${plugin.id}`} plugin={plugin} hideDescription />
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className="section-placeholder">{loading ? 'Loading popular plugins...' : 'Scroll to load popular plugins'}</div>
|
<div className="section-placeholder">{loading ? 'Loading popular plugins...' : 'Scroll to load popular plugins'}</div>
|
||||||
@ -232,7 +232,7 @@ const Home = () => {
|
|||||||
<div className="home-plugin-grid">
|
<div className="home-plugin-grid">
|
||||||
{newest.length > 0 ? (
|
{newest.length > 0 ? (
|
||||||
newest.map((plugin) => (
|
newest.map((plugin) => (
|
||||||
<PluginCard key={`new-${plugin.id}`} plugin={plugin} />
|
<PluginCard key={`new-${plugin.id}`} plugin={plugin} hideDescription />
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className="section-placeholder">{loading ? 'Loading new plugins...' : 'Scroll to load new plugins'}</div>
|
<div className="section-placeholder">{loading ? 'Loading new plugins...' : 'Scroll to load new plugins'}</div>
|
||||||
@ -245,7 +245,7 @@ const Home = () => {
|
|||||||
<div className="home-plugin-grid">
|
<div className="home-plugin-grid">
|
||||||
{allPlugins.length > 0 ? (
|
{allPlugins.length > 0 ? (
|
||||||
allPlugins.map((plugin) => (
|
allPlugins.map((plugin) => (
|
||||||
<PluginCard key={`all-${plugin.id}`} plugin={plugin} />
|
<PluginCard key={`all-${plugin.id}`} plugin={plugin} hideDescription />
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className="section-placeholder">{loading ? 'Loading all plugins...' : 'No plugins available'}</div>
|
<div className="section-placeholder">{loading ? 'Loading all plugins...' : 'No plugins available'}</div>
|
||||||
|
|||||||
@ -171,6 +171,51 @@ const ProfilePage = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSaveAccountInfo = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
await withSaving('accountInfo', async () => {
|
||||||
|
let updatedAny = false;
|
||||||
|
|
||||||
|
if (formData.username && formData.username !== user?.username) {
|
||||||
|
try {
|
||||||
|
const res = await api.post('/user/change-username', { newUsername: formData.username });
|
||||||
|
if (res.data.token) login(res.data.token);
|
||||||
|
updatedAny = true;
|
||||||
|
} catch (err: any) {
|
||||||
|
showToast(err.response?.data?.error || 'Failed to update username.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formData.email && formData.email !== user?.email) {
|
||||||
|
try {
|
||||||
|
const res = await api.post('/user/change-email', { newEmail: formData.email });
|
||||||
|
updatedAny = true;
|
||||||
|
} catch (err: any) {
|
||||||
|
showToast(err.response?.data?.error || 'Failed to update email.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formData.country && formData.country !== user?.country) {
|
||||||
|
try {
|
||||||
|
await api.post('/user/change-country', { newCountry: formData.country });
|
||||||
|
updatedAny = true;
|
||||||
|
} catch (err: any) {
|
||||||
|
showToast(err.response?.data?.error || 'Failed to update country.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updatedAny) {
|
||||||
|
showToast('Account details updated successfully!');
|
||||||
|
refreshUser?.();
|
||||||
|
} else {
|
||||||
|
showToast('No changes to save.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleLogout = () => { logout(); navigate('/login'); };
|
const handleLogout = () => { logout(); navigate('/login'); };
|
||||||
|
|
||||||
const handleConnectStripe = async () => {
|
const handleConnectStripe = async () => {
|
||||||
@ -375,11 +420,11 @@ const ProfilePage = () => {
|
|||||||
<h2 className="section-title" style={{ marginBottom: 0 }}>
|
<h2 className="section-title" style={{ marginBottom: 0 }}>
|
||||||
<span>Account</span> Details
|
<span>Account</span> Details
|
||||||
</h2>
|
</h2>
|
||||||
<p className="settings-section-sub">Manage your public identity and contact information.</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SettingsCard title="Username" description="Your public display name across the marketplace." icon={User}>
|
<SettingsCard title="Account Profile" icon={User}>
|
||||||
<form onSubmit={e => handleUpdate(e, '/user/change-username', { newUsername: formData.username }, 'username')}>
|
<form onSubmit={handleSaveAccountInfo} style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem' }}>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(260px, 1fr))', gap: '1.25rem' }}>
|
||||||
<Field label="Username">
|
<Field label="Username">
|
||||||
<input
|
<input
|
||||||
className="settings-input"
|
className="settings-input"
|
||||||
@ -387,36 +432,22 @@ const ProfilePage = () => {
|
|||||||
value={formData.username}
|
value={formData.username}
|
||||||
onChange={e => setFormData({ ...formData, username: e.target.value })}
|
onChange={e => setFormData({ ...formData, username: e.target.value })}
|
||||||
placeholder="Your username"
|
placeholder="Your username"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<SaveBtn id="username" label="Update Username" />
|
|
||||||
</form>
|
|
||||||
</SettingsCard>
|
|
||||||
|
|
||||||
<SettingsCard title="Email Address" description="We'll send a verification link to your new address." icon={Mail}>
|
<Field label="Email Address">
|
||||||
<form onSubmit={e => handleUpdate(e, '/user/change-email', { newEmail: formData.email }, 'email')}>
|
|
||||||
<Field label="Email">
|
|
||||||
<input
|
<input
|
||||||
className="settings-input"
|
className="settings-input"
|
||||||
type="email"
|
type="email"
|
||||||
value={formData.email}
|
value={formData.email}
|
||||||
onChange={e => setFormData({ ...formData, email: e.target.value })}
|
onChange={e => setFormData({ ...formData, email: e.target.value })}
|
||||||
placeholder="your@email.com"
|
placeholder="your@email.com"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<SaveBtn id="email" label="Update Email" />
|
</div>
|
||||||
</form>
|
|
||||||
</SettingsCard>
|
|
||||||
|
|
||||||
<SettingsCard title="Country" description="Used for tax purposes and regional features. Auto-detected from IP on registration." icon={Globe}>
|
|
||||||
<form onSubmit={e => {
|
|
||||||
if (!formData.country) {
|
|
||||||
e.preventDefault();
|
|
||||||
showToast('Please select a country.', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
handleUpdate(e, '/user/change-country', { newCountry: formData.country }, 'country');
|
|
||||||
}}>
|
|
||||||
<Field label="Country">
|
<Field label="Country">
|
||||||
<select
|
<select
|
||||||
className="settings-input settings-select"
|
className="settings-input settings-select"
|
||||||
@ -432,7 +463,10 @@ const ProfilePage = () => {
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</Field>
|
</Field>
|
||||||
<SaveBtn id="country" label="Update Country" />
|
|
||||||
|
<div style={{ marginTop: '0.5rem' }}>
|
||||||
|
<SaveBtn id="accountInfo" label="Save Changes" />
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -13,6 +13,44 @@
|
|||||||
--dash-shadow: 0 1px 3px rgba(0,0,0,0.08); /* New: for subtle shadows */
|
--dash-shadow: 0 1px 3px rgba(0,0,0,0.08); /* New: for subtle shadows */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Dashboard Top App Header */
|
||||||
|
.dashboard-app-header {
|
||||||
|
background: var(--dash-surface);
|
||||||
|
border-bottom: 1px solid var(--dash-border);
|
||||||
|
padding: 0.85rem 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-app-header-inner {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-app-brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
text-decoration: none !important;
|
||||||
|
color: var(--dash-text);
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
letter-spacing: -0.3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-app-logo {
|
||||||
|
height: 32px;
|
||||||
|
width: auto;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-app-title span {
|
||||||
|
color: var(--dash-accent, #ff7518);
|
||||||
|
}
|
||||||
|
|
||||||
/* Override App.css within the dashboard */
|
/* Override App.css within the dashboard */
|
||||||
.dashboard-content, .dashboard-nav {
|
.dashboard-content, .dashboard-nav {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
||||||
|
|||||||
@ -3,10 +3,11 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import {
|
import {
|
||||||
Globe, ImageIcon, Plus, Search, X, Upload, FileCode,
|
Globe, ImageIcon, Plus, Search, X, Upload, FileCode,
|
||||||
DollarSign, CheckCircle, ChevronRight, ChevronLeft,
|
DollarSign, CheckCircle, ChevronRight, ChevronLeft,
|
||||||
Zap, Lock, AlertTriangle, Tag, Store, User,
|
Zap, Lock, AlertTriangle, Tag, Store, User, Sparkles,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import api from '../../../api';
|
import api from '../../../api';
|
||||||
import { useAuth } from '../../../App';
|
import { useAuth } from '../../../App';
|
||||||
|
import { DeveloperOnboardingModal } from '../../../components/DeveloperOnboardingModal';
|
||||||
import './AddPlugin.css';
|
import './AddPlugin.css';
|
||||||
|
|
||||||
// ── Locale helpers ────────────────────────────────────────────────────────────
|
// ── Locale helpers ────────────────────────────────────────────────────────────
|
||||||
@ -193,6 +194,7 @@ const LangPicker = ({ usedCodes, onAdd, onClose }: { usedCodes: string[]; onAdd:
|
|||||||
const AddPlugin = () => {
|
const AddPlugin = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { user, refreshUser } = useAuth();
|
const { user, refreshUser } = useAuth();
|
||||||
|
const [isDevModalOpen, setIsDevModalOpen] = useState(false);
|
||||||
const [step, setStep] = useState(0);
|
const [step, setStep] = useState(0);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [done, setDone] = useState(false);
|
const [done, setDone] = useState(false);
|
||||||
@ -205,6 +207,45 @@ const AddPlugin = () => {
|
|||||||
const [youtubeVideoUrl, setYoutubeVideoUrl] = useState('');
|
const [youtubeVideoUrl, setYoutubeVideoUrl] = useState('');
|
||||||
const [keywords, setKeywords] = useState('');
|
const [keywords, setKeywords] = useState('');
|
||||||
|
|
||||||
|
if (user && !user.is_developer) {
|
||||||
|
return (
|
||||||
|
<div className="ap-root" style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '60vh', padding: '2rem 1rem' }}>
|
||||||
|
<div style={{
|
||||||
|
maxWidth: 520, width: '100%', padding: '2.5rem', background: 'var(--mp-surface, #14171c)',
|
||||||
|
borderRadius: 16, border: '1px solid var(--mp-border, rgba(255,255,255,0.1))', textAlign: 'center',
|
||||||
|
boxShadow: '0 8px 32px rgba(0,0,0,0.3)'
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
width: 60, height: 60, borderRadius: '50%', background: 'rgba(249, 115, 22, 0.15)',
|
||||||
|
color: 'var(--mp-accent, #f97316)', display: 'inline-flex', alignItems: 'center',
|
||||||
|
justifyContent: 'center', marginBottom: '1.25rem'
|
||||||
|
}}>
|
||||||
|
<Sparkles size={28} />
|
||||||
|
</div>
|
||||||
|
<h2 style={{ fontSize: '1.5rem', fontWeight: 700, margin: '0 0 0.5rem 0', color: 'var(--mp-text, #fff)' }}>
|
||||||
|
Developer Profile Required
|
||||||
|
</h2>
|
||||||
|
<p style={{ color: 'var(--mp-muted, #94a3b8)', fontSize: '0.9rem', lineHeight: 1.5, marginBottom: '1.75rem' }}>
|
||||||
|
Before publishing plugins on Pumpkin Market, you must complete your developer profile. It takes less than a minute!
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
className="mp-btn mp-btn-primary"
|
||||||
|
style={{ width: '100%', padding: '0.85rem', fontSize: '0.95rem', fontWeight: 600, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '0.5rem', cursor: 'pointer' }}
|
||||||
|
onClick={() => setIsDevModalOpen(true)}
|
||||||
|
>
|
||||||
|
<Sparkles size={16} /> Set Up Developer Profile
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<DeveloperOnboardingModal
|
||||||
|
isOpen={isDevModalOpen}
|
||||||
|
onClose={() => setIsDevModalOpen(false)}
|
||||||
|
onSuccess={() => refreshUser?.()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Step 1: Store Listing ──
|
// ── Step 1: Store Listing ──
|
||||||
const [descriptions, setDescriptions] = useState<Record<string, string>>({ [DEFAULT_LOCALE]: '' });
|
const [descriptions, setDescriptions] = useState<Record<string, string>>({ [DEFAULT_LOCALE]: '' });
|
||||||
const [activeLocale, setActiveLocale] = useState(DEFAULT_LOCALE);
|
const [activeLocale, setActiveLocale] = useState(DEFAULT_LOCALE);
|
||||||
|
|||||||
251
src/pages/dashboard/plugin/Licenses.tsx
Normal file
251
src/pages/dashboard/plugin/Licenses.tsx
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Key, Plus, Trash2, ShieldAlert, ShieldCheck, Copy, Check, UserCheck } from 'lucide-react';
|
||||||
|
import api from '../../../api';
|
||||||
|
import type { PluginData } from './ManagePlugin';
|
||||||
|
|
||||||
|
interface PluginLicense {
|
||||||
|
id: number;
|
||||||
|
plugin_id: number;
|
||||||
|
user_id: number;
|
||||||
|
username: string;
|
||||||
|
email: string | null;
|
||||||
|
license_key: string;
|
||||||
|
revoked: boolean;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = { plugin: PluginData };
|
||||||
|
|
||||||
|
const Licenses = ({ plugin }: Props) => {
|
||||||
|
const [licenses, setLicenses] = useState<PluginLicense[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
// Form state
|
||||||
|
const [userIdentifier, setUserIdentifier] = useState('');
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
const [copiedKey, setCopiedKey] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchLicenses();
|
||||||
|
}, [plugin.id]);
|
||||||
|
|
||||||
|
const fetchLicenses = async () => {
|
||||||
|
try {
|
||||||
|
const res = await api.get(`/plugins/${plugin.id}/licenses`);
|
||||||
|
setLicenses(res.data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch licenses', err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGrantLicense = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!userIdentifier.trim()) return;
|
||||||
|
setSubmitting(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await api.post(`/plugins/${plugin.id}/licenses`, {
|
||||||
|
user_identifier: userIdentifier.trim()
|
||||||
|
});
|
||||||
|
setLicenses(prev => [res.data, ...prev.filter(l => l.id !== res.data.id)]);
|
||||||
|
setUserIdentifier('');
|
||||||
|
} catch (err: any) {
|
||||||
|
alert(err.response?.data || 'Failed to grant license to user.');
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleRevoke = async (licenseId: number, currentRevoked: boolean) => {
|
||||||
|
const action = currentRevoked ? 'unrevoke' : 'revoke';
|
||||||
|
if (!window.confirm(`Are you sure you want to ${action} this license key?`)) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.put(`/plugins/${plugin.id}/licenses/${licenseId}/revoke`, {
|
||||||
|
revoked: !currentRevoked
|
||||||
|
});
|
||||||
|
setLicenses(prev =>
|
||||||
|
prev.map(l => l.id === licenseId ? { ...l, revoked: !currentRevoked } : l)
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
alert(`Failed to ${action} license.`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (licenseId: number) => {
|
||||||
|
if (!window.confirm('Are you sure you want to permanently delete this license key record?')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.delete(`/plugins/${plugin.id}/licenses/${licenseId}`);
|
||||||
|
setLicenses(prev => prev.filter(l => l.id !== licenseId));
|
||||||
|
} catch (err) {
|
||||||
|
alert('Failed to delete license record.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopyKey = (key: string) => {
|
||||||
|
navigator.clipboard.writeText(key);
|
||||||
|
setCopiedKey(key);
|
||||||
|
setTimeout(() => setCopiedKey(null), 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
|
||||||
|
<div className="mp-section-header">
|
||||||
|
<h2 className="mp-title">License Key Management</h2>
|
||||||
|
<p>View, grant, revoke, and manage license keys for your paid plugin buyers and testers.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(320px, 1fr))', gap: '2rem' }}>
|
||||||
|
{/* Grant License Card */}
|
||||||
|
<div className="mp-card" style={{ height: 'fit-content' }}>
|
||||||
|
<div className="mp-card-title">
|
||||||
|
<Plus size={14} /> Grant Manual License
|
||||||
|
</div>
|
||||||
|
<p style={{ fontSize: '0.85rem', color: 'var(--mp-muted)', marginBottom: '1rem' }}>
|
||||||
|
Issue a valid license key to a user by entering their username, email address, or account ID.
|
||||||
|
</p>
|
||||||
|
<form onSubmit={handleGrantLicense} style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem' }}>
|
||||||
|
<div className="mp-form-group">
|
||||||
|
<label className="mp-label">User (Username, Email, or User ID)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="mp-input"
|
||||||
|
placeholder="e.g. alex or alex@example.com"
|
||||||
|
value={userIdentifier}
|
||||||
|
onChange={e => setUserIdentifier(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="mp-btn mp-btn-primary"
|
||||||
|
disabled={submitting}
|
||||||
|
style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '0.5rem' }}
|
||||||
|
>
|
||||||
|
<Key size={14} />
|
||||||
|
{submitting ? 'Granting...' : 'Issue License Key'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Licenses Table / List */}
|
||||||
|
<div className="mp-card" style={{ gridColumn: 'span 2' }}>
|
||||||
|
<div className="mp-card-title" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<span style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||||
|
<Key size={14} /> Active & Issued Licenses ({licenses.length})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<p style={{ color: 'var(--mp-muted)', fontSize: '0.9rem', padding: '1rem 0' }}>Loading licenses…</p>
|
||||||
|
) : licenses.length === 0 ? (
|
||||||
|
<div style={{ textAlign: 'center', padding: '3rem 1rem', color: 'var(--mp-muted)' }}>
|
||||||
|
<Key size={32} style={{ marginBottom: '0.5rem', opacity: 0.5 }} />
|
||||||
|
<p>No license keys issued yet for this plugin.</p>
|
||||||
|
<p style={{ fontSize: '0.85rem' }}>Licenses will automatically appear here when customers purchase your plugin.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ overflowX: 'auto', marginTop: '1rem' }}>
|
||||||
|
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.875rem' }}>
|
||||||
|
<thead>
|
||||||
|
<tr style={{ borderBottom: '1px solid var(--mp-border)', textAlign: 'left', color: 'var(--mp-muted)' }}>
|
||||||
|
<th style={{ padding: '0.75rem' }}>User</th>
|
||||||
|
<th style={{ padding: '0.75rem' }}>License Key</th>
|
||||||
|
<th style={{ padding: '0.75rem' }}>Issued Date</th>
|
||||||
|
<th style={{ padding: '0.75rem' }}>Status</th>
|
||||||
|
<th style={{ padding: '0.75rem', textAlign: 'right' }}>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{licenses.map(lic => (
|
||||||
|
<tr key={lic.id} style={{ borderBottom: '1px solid var(--mp-border)', opacity: lic.revoked ? 0.65 : 1 }}>
|
||||||
|
<td style={{ padding: '0.75rem' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontWeight: 500 }}>
|
||||||
|
<UserCheck size={14} color="var(--mp-accent)" />
|
||||||
|
{lic.username}
|
||||||
|
</div>
|
||||||
|
{lic.email && (
|
||||||
|
<div style={{ fontSize: '0.75rem', color: 'var(--mp-muted)' }}>{lic.email}</div>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: '0.75rem' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontFamily: 'monospace' }}>
|
||||||
|
<span>{lic.license_key}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleCopyKey(lic.license_key)}
|
||||||
|
title="Copy license key"
|
||||||
|
style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--mp-muted)' }}
|
||||||
|
>
|
||||||
|
{copiedKey === lic.license_key ? <Check size={13} color="var(--mp-success)" /> : <Copy size={13} />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: '0.75rem', color: 'var(--mp-muted)', fontSize: '0.8rem' }}>
|
||||||
|
{new Date(lic.created_at).toLocaleDateString()}
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: '0.75rem' }}>
|
||||||
|
{lic.revoked ? (
|
||||||
|
<span style={{
|
||||||
|
display: 'inline-flex', alignItems: 'center', gap: '0.25rem',
|
||||||
|
padding: '0.2rem 0.5rem', borderRadius: '4px', fontSize: '0.75rem',
|
||||||
|
background: 'rgba(239, 68, 68, 0.15)', color: '#f87171'
|
||||||
|
}}>
|
||||||
|
<ShieldAlert size={12} /> Revoked
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span style={{
|
||||||
|
display: 'inline-flex', alignItems: 'center', gap: '0.25rem',
|
||||||
|
padding: '0.2rem 0.5rem', borderRadius: '4px', fontSize: '0.75rem',
|
||||||
|
background: 'rgba(34, 197, 94, 0.15)', color: '#4ade80'
|
||||||
|
}}>
|
||||||
|
<ShieldCheck size={12} /> Active
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: '0.75rem', textAlign: 'right' }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.5rem' }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleToggleRevoke(lic.id, lic.revoked)}
|
||||||
|
className="mp-btn"
|
||||||
|
style={{
|
||||||
|
padding: '0.3rem 0.6rem',
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
background: lic.revoked ? 'var(--mp-accent)' : 'rgba(239, 68, 68, 0.2)',
|
||||||
|
color: lic.revoked ? '#fff' : '#f87171',
|
||||||
|
border: 'none', borderRadius: '4px', cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{lic.revoked ? 'Unrevoke' : 'Revoke'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleDelete(lic.id)}
|
||||||
|
title="Delete License"
|
||||||
|
style={{
|
||||||
|
background: 'none', border: 'none', cursor: 'pointer',
|
||||||
|
color: 'var(--mp-muted)', padding: '0.3rem'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 size={14} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Licenses;
|
||||||
@ -3,13 +3,15 @@ import { useNavigate, useParams } from 'react-router-dom';
|
|||||||
import api from '../../../api';
|
import api from '../../../api';
|
||||||
import {
|
import {
|
||||||
LayoutGrid, Tag, Upload, DollarSign, Trash2,
|
LayoutGrid, Tag, Upload, DollarSign, Trash2,
|
||||||
Circle, BarChart3
|
Circle, BarChart3, Key, Star
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
import StoreListing from './StoreListing';
|
import StoreListing from './StoreListing';
|
||||||
import PublishUpdate from './PublishUpdate';
|
import PublishUpdate from './PublishUpdate';
|
||||||
import Pricing from './Pricing';
|
import Pricing from './Pricing';
|
||||||
import Coupons from './Coupons';
|
import Coupons from './Coupons';
|
||||||
|
import Licenses from './Licenses';
|
||||||
|
import PluginReviewsTab from './PluginReviewsTab';
|
||||||
import DangerZone from './DangerZone';
|
import DangerZone from './DangerZone';
|
||||||
import PluginAnalyticsTab from './PluginAnalyticsTab';
|
import PluginAnalyticsTab from './PluginAnalyticsTab';
|
||||||
import './ManagePlugin.css';
|
import './ManagePlugin.css';
|
||||||
@ -37,7 +39,9 @@ const NAV_ITEMS = [
|
|||||||
{ key: 'listing', label: 'Store Listing', icon: LayoutGrid },
|
{ key: 'listing', label: 'Store Listing', icon: LayoutGrid },
|
||||||
{ key: 'update', label: 'Publish Update', icon: Upload },
|
{ key: 'update', label: 'Publish Update', icon: Upload },
|
||||||
{ key: 'analytics', label: 'Analytics', icon: BarChart3 },
|
{ key: 'analytics', label: 'Analytics', icon: BarChart3 },
|
||||||
|
{ key: 'reviews', label: 'Reviews', icon: Star },
|
||||||
{ key: 'pricing', label: 'Pricing', icon: DollarSign },
|
{ key: 'pricing', label: 'Pricing', icon: DollarSign },
|
||||||
|
{ key: 'licenses', label: 'Licenses', icon: Key },
|
||||||
{ key: 'coupons', label: 'Coupons', icon: Tag },
|
{ key: 'coupons', label: 'Coupons', icon: Tag },
|
||||||
{ key: 'danger', label: 'Danger Zone', icon: Trash2 },
|
{ key: 'danger', label: 'Danger Zone', icon: Trash2 },
|
||||||
];
|
];
|
||||||
@ -136,9 +140,15 @@ const ManagePlugin = () => {
|
|||||||
{activeTab === 'analytics' && (
|
{activeTab === 'analytics' && (
|
||||||
<PluginAnalyticsTab pluginId={plugin.id} pluginName={plugin.name} />
|
<PluginAnalyticsTab pluginId={plugin.id} pluginName={plugin.name} />
|
||||||
)}
|
)}
|
||||||
|
{activeTab === 'reviews' && (
|
||||||
|
<PluginReviewsTab plugin={plugin} />
|
||||||
|
)}
|
||||||
{activeTab === 'pricing' && (
|
{activeTab === 'pricing' && (
|
||||||
<Pricing plugin={plugin} onSaved={refreshPlugin} />
|
<Pricing plugin={plugin} onSaved={refreshPlugin} />
|
||||||
)}
|
)}
|
||||||
|
{activeTab === 'licenses' && (
|
||||||
|
<Licenses plugin={plugin} />
|
||||||
|
)}
|
||||||
{activeTab === 'coupons' && (
|
{activeTab === 'coupons' && (
|
||||||
<Coupons plugin={plugin} />
|
<Coupons plugin={plugin} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
266
src/pages/dashboard/plugin/PluginReviewsTab.tsx
Normal file
266
src/pages/dashboard/plugin/PluginReviewsTab.tsx
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
|
import { Star, MessageSquare, CornerDownRight, Send, CheckCircle2 } from 'lucide-react';
|
||||||
|
import api from '../../../api';
|
||||||
|
import type { PluginData } from './ManagePlugin';
|
||||||
|
|
||||||
|
interface Review {
|
||||||
|
id: number;
|
||||||
|
user_id: number;
|
||||||
|
plugin_id: number;
|
||||||
|
rating: number;
|
||||||
|
comment: string;
|
||||||
|
developer_reply?: string | null;
|
||||||
|
replied_at?: string | null;
|
||||||
|
created_at: string;
|
||||||
|
username: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
plugin: PluginData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PluginReviewsTab = ({ plugin }: Props) => {
|
||||||
|
const [reviews, setReviews] = useState<Review[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [replyText, setReplyText] = useState<{ [reviewId: number]: string }>({});
|
||||||
|
const [submitting, setSubmitting] = useState<{ [reviewId: number]: boolean }>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchPluginReviews();
|
||||||
|
}, [plugin.id]);
|
||||||
|
|
||||||
|
const fetchPluginReviews = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await api.get(`/plugins/${plugin.id}`);
|
||||||
|
const fetchedReviews: Review[] = res.data.reviews || [];
|
||||||
|
setReviews(fetchedReviews);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch plugin reviews', err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate rating statistics & distribution
|
||||||
|
const stats = useMemo(() => {
|
||||||
|
if (reviews.length === 0) {
|
||||||
|
return {
|
||||||
|
average: 0,
|
||||||
|
total: 0,
|
||||||
|
distribution: { 5: 0, 4: 0, 3: 0, 2: 0, 1: 0 }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = reviews.length;
|
||||||
|
const sum = reviews.reduce((acc, r) => acc + r.rating, 0);
|
||||||
|
const average = Number((sum / total).toFixed(1));
|
||||||
|
|
||||||
|
const distribution: { [key: number]: number } = { 5: 0, 4: 0, 3: 0, 2: 0, 1: 0 };
|
||||||
|
reviews.forEach(r => {
|
||||||
|
if (distribution[r.rating] !== undefined) {
|
||||||
|
distribution[r.rating]++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { average, total, distribution };
|
||||||
|
}, [reviews]);
|
||||||
|
|
||||||
|
const handleSendReply = async (reviewId: number) => {
|
||||||
|
const text = replyText[reviewId]?.trim();
|
||||||
|
if (!text) return;
|
||||||
|
|
||||||
|
setSubmitting(s => ({ ...s, [reviewId]: true }));
|
||||||
|
try {
|
||||||
|
await api.post(`/plugins/${plugin.id}/reviews/${reviewId}/reply`, { reply: text });
|
||||||
|
setReviews(prev =>
|
||||||
|
prev.map(r =>
|
||||||
|
r.id === reviewId
|
||||||
|
? { ...r, developer_reply: text, replied_at: new Date().toISOString() }
|
||||||
|
: r
|
||||||
|
)
|
||||||
|
);
|
||||||
|
setReplyText(t => ({ ...t, [reviewId]: '' }));
|
||||||
|
} catch (err: any) {
|
||||||
|
alert(err.response?.data?.error || err.response?.data || 'Failed to submit reply.');
|
||||||
|
} finally {
|
||||||
|
setSubmitting(s => ({ ...s, [reviewId]: false }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderStars = (rating: number, size = 14) => (
|
||||||
|
<div style={{ display: 'flex', gap: '2px', alignItems: 'center' }}>
|
||||||
|
{[1, 2, 3, 4, 5].map(star => (
|
||||||
|
<Star
|
||||||
|
key={star}
|
||||||
|
size={size}
|
||||||
|
fill={star <= rating ? '#f59e0b' : 'transparent'}
|
||||||
|
color={star <= rating ? '#f59e0b' : 'var(--mp-border)'}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mp-section-header">
|
||||||
|
<h2 className="mp-title">Reviews & Ratings</h2>
|
||||||
|
<p>Monitor user feedback, rating trends, and reply directly to customer reviews.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div style={{ color: 'var(--mp-muted)', padding: '2rem 0' }}>Loading plugin reviews…</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* Rating Overview Cards */}
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: '1.5rem' }}>
|
||||||
|
{/* Summary Box */}
|
||||||
|
<div className="mp-card" style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', textAlign: 'center', padding: '2rem' }}>
|
||||||
|
<div style={{ fontSize: '3.2rem', fontWeight: 800, color: 'var(--mp-text)', lineHeight: 1 }}>
|
||||||
|
{stats.average > 0 ? stats.average : 'N/A'}
|
||||||
|
</div>
|
||||||
|
<div style={{ margin: '0.75rem 0' }}>
|
||||||
|
{renderStars(Math.round(stats.average), 18)}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '0.875rem', color: 'var(--mp-muted)' }}>
|
||||||
|
Based on {stats.total} {stats.total === 1 ? 'review' : 'reviews'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Breakdown Bars */}
|
||||||
|
<div className="mp-card" style={{ flex: 1 }}>
|
||||||
|
<div className="mp-card-title" style={{ marginBottom: '1rem' }}>
|
||||||
|
Rating Breakdown
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.6rem' }}>
|
||||||
|
{[5, 4, 3, 2, 1].map(starsCount => {
|
||||||
|
const count = stats.distribution[starsCount] || 0;
|
||||||
|
const pct = stats.total > 0 ? Math.round((count / stats.total) * 100) : 0;
|
||||||
|
return (
|
||||||
|
<div key={starsCount} style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', fontSize: '0.85rem' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '4px', width: '50px', color: 'var(--mp-muted)' }}>
|
||||||
|
<span>{starsCount}</span>
|
||||||
|
<Star size={12} fill="#f59e0b" color="#f59e0b" />
|
||||||
|
</div>
|
||||||
|
<div style={{ flex: 1, height: '8px', background: 'rgba(255, 255, 255, 0.08)', borderRadius: '4px', overflow: 'hidden' }}>
|
||||||
|
<div style={{ width: `${pct}%`, height: '100%', background: '#f59e0b', borderRadius: '4px', transition: 'width 0.3s' }} />
|
||||||
|
</div>
|
||||||
|
<div style={{ width: '40px', textAlign: 'right', color: 'var(--mp-muted)', fontSize: '0.8rem' }}>
|
||||||
|
{pct}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Reviews List */}
|
||||||
|
<div className="mp-card">
|
||||||
|
<div className="mp-card-title" style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '1.5rem' }}>
|
||||||
|
<MessageSquare size={16} /> All Reviews ({reviews.length})
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{reviews.length === 0 ? (
|
||||||
|
<div style={{ textAlign: 'center', padding: '3rem 1rem', color: 'var(--mp-muted)' }}>
|
||||||
|
<MessageSquare size={36} style={{ marginBottom: '0.75rem', opacity: 0.4 }} />
|
||||||
|
<p style={{ fontWeight: 600, margin: '0 0 0.25rem 0' }}>No reviews yet</p>
|
||||||
|
<p style={{ fontSize: '0.85rem' }}>When users review this plugin, their feedback and ratings will appear here.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
|
||||||
|
{reviews.map(rev => (
|
||||||
|
<div
|
||||||
|
key={rev.id}
|
||||||
|
style={{
|
||||||
|
padding: '1.25rem',
|
||||||
|
borderRadius: '8px',
|
||||||
|
background: 'rgba(255, 255, 255, 0.03)',
|
||||||
|
border: '1px solid var(--mp-border)',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '0.75rem'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontWeight: 600, fontSize: '0.95rem', color: 'var(--mp-text)' }}>
|
||||||
|
{rev.username}
|
||||||
|
</div>
|
||||||
|
<div style={{ marginTop: '0.25rem' }}>
|
||||||
|
{renderStars(rev.rating)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '0.8rem', color: 'var(--mp-muted)' }}>
|
||||||
|
{new Date(rev.created_at).toLocaleDateString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style={{ margin: 0, fontSize: '0.9rem', color: 'var(--mp-text)', lineHeight: 1.5, whiteSpace: 'pre-wrap' }}>
|
||||||
|
{rev.comment}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Developer Reply Box or Input */}
|
||||||
|
{rev.developer_reply ? (
|
||||||
|
<div style={{
|
||||||
|
marginTop: '0.5rem',
|
||||||
|
padding: '1rem',
|
||||||
|
borderRadius: '6px',
|
||||||
|
background: 'rgba(34, 197, 94, 0.08)',
|
||||||
|
borderLeft: '3px solid var(--mp-success)',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '0.4rem'
|
||||||
|
}}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', fontSize: '0.8rem', fontWeight: 600, color: 'var(--mp-success)' }}>
|
||||||
|
<CheckCircle2 size={13} /> Developer Response
|
||||||
|
{rev.replied_at && (
|
||||||
|
<span style={{ fontWeight: 400, color: 'var(--mp-muted)', marginLeft: 'auto' }}>
|
||||||
|
{new Date(rev.replied_at).toLocaleDateString()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p style={{ margin: 0, fontSize: '0.875rem', color: 'var(--mp-text)', lineHeight: 1.4 }}>
|
||||||
|
{rev.developer_reply}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ marginTop: '0.5rem', display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', fontSize: '0.8rem', color: 'var(--mp-muted)' }}>
|
||||||
|
<CornerDownRight size={13} /> Respond to {rev.username}:
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||||
|
<textarea
|
||||||
|
className="mp-input"
|
||||||
|
rows={2}
|
||||||
|
placeholder="Write your official response..."
|
||||||
|
value={replyText[rev.id] || ''}
|
||||||
|
onChange={e => setReplyText({ ...replyText, [rev.id]: e.target.value })}
|
||||||
|
style={{ flex: 1, resize: 'vertical', fontSize: '0.85rem' }}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="mp-btn mp-btn-primary"
|
||||||
|
disabled={submitting[rev.id] || !replyText[rev.id]?.trim()}
|
||||||
|
onClick={() => handleSendReply(rev.id)}
|
||||||
|
style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', height: 'fit-content', padding: '0.6rem 1rem' }}
|
||||||
|
>
|
||||||
|
<Send size={13} />
|
||||||
|
{submitting[rev.id] ? 'Posting…' : 'Reply'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PluginReviewsTab;
|
||||||
Loading…
Reference in New Issue
Block a user