add: the milestone route, a chart demo for data visualization
This commit is contained in:
parent
02a2a845da
commit
f511a2bfdf
160
bun.lock
160
bun.lock
@ -78,6 +78,7 @@
|
||||
"dependencies": {
|
||||
"@elysiajs/cors": "^1.4.0",
|
||||
"@elysiajs/openapi": "^1.4.0",
|
||||
"@elysiajs/server-timing": "^1.4.0",
|
||||
"chalk": "^5.6.2",
|
||||
"elysia": "^1.4.0",
|
||||
"zod": "^4.1.11",
|
||||
@ -185,6 +186,8 @@
|
||||
"name": "@cvsa/cvsa-temp",
|
||||
"dependencies": {
|
||||
"@elysiajs/eden": "^1.4.1",
|
||||
"@nivo/core": "^0.99.0",
|
||||
"@nivo/line": "^0.99.0",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@react-router/node": "^7.7.1",
|
||||
"@react-router/serve": "^7.7.1",
|
||||
@ -196,6 +199,7 @@
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-router": "^7.7.1",
|
||||
"recharts": "^3.2.1",
|
||||
"sonner": "^2.0.7",
|
||||
"swr": "^2.3.6",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
@ -356,6 +360,8 @@
|
||||
|
||||
"@elysiajs/openapi": ["@elysiajs/openapi@1.4.10", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-bEsETp/CGcs1CqH3zW6/CAI2g6d0K/g8wUuH7HwXQm0gtP18s9RnljJESuv4of3ePUoYQgy85t+dha+ABv+L/A=="],
|
||||
|
||||
"@elysiajs/server-timing": ["@elysiajs/server-timing@1.4.0", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-vDFdHyi8Q43vgA5MaTQMA9v4/bgKrtqPrpVqVuHlMCRQgfOpvYGXPj3okSttyendG5r2bRHfyPG11lTWWIrzrQ=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="],
|
||||
@ -538,6 +544,28 @@
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ=="],
|
||||
|
||||
"@nivo/annotations": ["@nivo/annotations@0.99.0", "", { "dependencies": { "@nivo/colors": "0.99.0", "@nivo/core": "0.99.0", "@nivo/theming": "0.99.0", "@react-spring/web": "9.4.5 || ^9.7.2 || ^10.0", "lodash": "^4.17.21" }, "peerDependencies": { "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" } }, "sha512-jCuuXPbvpaqaz4xF7k5dv0OT2ubn5Nt0gWryuTe/8oVsC/9bzSuK8bM9vBty60m9tfO+X8vUYliuaCDwGksC2g=="],
|
||||
|
||||
"@nivo/axes": ["@nivo/axes@0.99.0", "", { "dependencies": { "@nivo/core": "0.99.0", "@nivo/scales": "0.99.0", "@nivo/text": "0.99.0", "@nivo/theming": "0.99.0", "@react-spring/web": "9.4.5 || ^9.7.2 || ^10.0", "@types/d3-format": "^1.4.1", "@types/d3-time-format": "^2.3.1", "d3-format": "^1.4.4", "d3-time-format": "^3.0.0" }, "peerDependencies": { "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" } }, "sha512-3KschnmEL0acRoa7INSSOSEFwJLm54aZwSev7/r8XxXlkgRBriu6ReZy/FG0wfN+ljZ4GMvx+XyIIf6kxzvrZg=="],
|
||||
|
||||
"@nivo/colors": ["@nivo/colors@0.99.0", "", { "dependencies": { "@nivo/core": "0.99.0", "@nivo/theming": "0.99.0", "@types/d3-color": "^3.0.0", "@types/d3-scale": "^4.0.8", "@types/d3-scale-chromatic": "^3.0.0", "d3-color": "^3.1.0", "d3-scale": "^4.0.2", "d3-scale-chromatic": "^3.0.0", "lodash": "^4.17.21" }, "peerDependencies": { "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" } }, "sha512-hyYt4lEFIfXOUmQ6k3HXm3KwhcgoJpocmoGzLUqzk7DzuhQYJo+4d5jIGGU0N/a70+9XbHIdpKNSblHAIASD3w=="],
|
||||
|
||||
"@nivo/core": ["@nivo/core@0.99.0", "", { "dependencies": { "@nivo/theming": "0.99.0", "@nivo/tooltip": "0.99.0", "@react-spring/web": "9.4.5 || ^9.7.2 || ^10.0", "@types/d3-shape": "^3.1.6", "d3-color": "^3.1.0", "d3-format": "^1.4.4", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-scale-chromatic": "^3.0.0", "d3-shape": "^3.2.0", "d3-time-format": "^3.0.0", "lodash": "^4.17.21", "react-virtualized-auto-sizer": "^1.0.26", "use-debounce": "^10.0.4" }, "peerDependencies": { "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" } }, "sha512-olCItqhPG3xHL5ei+vg52aB6o+6S+xR2idpkd9RormTTUniZb8U2rOdcQojOojPY5i9kVeQyLFBpV4YfM7OZ9g=="],
|
||||
|
||||
"@nivo/legends": ["@nivo/legends@0.99.0", "", { "dependencies": { "@nivo/colors": "0.99.0", "@nivo/core": "0.99.0", "@nivo/text": "0.99.0", "@nivo/theming": "0.99.0", "@types/d3-scale": "^4.0.8", "d3-scale": "^4.0.2" }, "peerDependencies": { "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" } }, "sha512-P16FjFqNceuTTZphINAh5p0RF0opu3cCKoWppe2aRD9IuVkvRm/wS5K1YwMCxDzKyKh5v0AuTlu9K6o3/hk8hA=="],
|
||||
|
||||
"@nivo/line": ["@nivo/line@0.99.0", "", { "dependencies": { "@nivo/annotations": "0.99.0", "@nivo/axes": "0.99.0", "@nivo/colors": "0.99.0", "@nivo/core": "0.99.0", "@nivo/legends": "0.99.0", "@nivo/scales": "0.99.0", "@nivo/theming": "0.99.0", "@nivo/tooltip": "0.99.0", "@nivo/voronoi": "0.99.0", "@react-spring/web": "9.4.5 || ^9.7.2 || ^10.0", "@types/d3-shape": "^3.1.6", "d3-shape": "^3.2.0" }, "peerDependencies": { "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" } }, "sha512-bAqTXSjpnpcGMs341qWFUi7hJTqQiNoSeJHsYPuPS3icuXPcp3WETQH+zRZACeEF79ZigeOWCW+dzODgne1y9w=="],
|
||||
|
||||
"@nivo/scales": ["@nivo/scales@0.99.0", "", { "dependencies": { "@types/d3-interpolate": "^3.0.4", "@types/d3-scale": "^4.0.8", "@types/d3-time": "^1.1.1", "@types/d3-time-format": "^3.0.0", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-time": "^1.0.11", "d3-time-format": "^3.0.0", "lodash": "^4.17.21" } }, "sha512-g/2K4L6L8si6E2BWAHtFVGahtDKbUcO6xHJtlIZMwdzaJc7yB16EpWLK8AfI/A42KadLhJSJqBK3mty+c7YZ+w=="],
|
||||
|
||||
"@nivo/text": ["@nivo/text@0.99.0", "", { "dependencies": { "@nivo/core": "0.99.0", "@nivo/theming": "0.99.0", "@react-spring/web": "9.4.5 || ^9.7.2 || ^10.0" }, "peerDependencies": { "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" } }, "sha512-ho3oZpAZApsJNjsIL5WJSAdg/wjzTBcwo1KiHBlRGUmD+yUWO8qp7V+mnYRhJchwygtRVALlPgZ/rlcW2Xr/MQ=="],
|
||||
|
||||
"@nivo/theming": ["@nivo/theming@0.99.0", "", { "dependencies": { "lodash": "^4.17.21" }, "peerDependencies": { "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" } }, "sha512-KvXlf0nqBzh/g2hAIV9bzscYvpq1uuO3TnFN3RDXGI72CrbbZFTGzprPju3sy/myVsauv+Bb+V4f5TZ0jkYKRg=="],
|
||||
|
||||
"@nivo/tooltip": ["@nivo/tooltip@0.99.0", "", { "dependencies": { "@nivo/core": "0.99.0", "@nivo/theming": "0.99.0", "@react-spring/web": "9.4.5 || ^9.7.2 || ^10.0" }, "peerDependencies": { "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" } }, "sha512-weoEGR3xAetV4k2P6k96cdamGzKQ5F2Pq+uyDaHr1P3HYArM879Pl+x+TkU0aWjP6wgUZPx/GOBiV1Hb1JxIqg=="],
|
||||
|
||||
"@nivo/voronoi": ["@nivo/voronoi@0.99.0", "", { "dependencies": { "@nivo/core": "0.99.0", "@nivo/theming": "0.99.0", "@nivo/tooltip": "0.99.0", "@types/d3-delaunay": "^6.0.4", "@types/d3-scale": "^4.0.8", "d3-delaunay": "^6.0.4", "d3-scale": "^4.0.2" }, "peerDependencies": { "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" } }, "sha512-KfmMdidbYzhiUCki1FG4X4nHEFT4loK8G5bMBnmCl9U+S78W+gvkfrgD2Aoqp/Q9yKQvr3Y8UcZKSFZnn3HgjQ=="],
|
||||
|
||||
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
||||
|
||||
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
|
||||
@ -650,6 +678,20 @@
|
||||
|
||||
"@react-router/serve": ["@react-router/serve@7.9.1", "", { "dependencies": { "@react-router/express": "7.9.1", "@react-router/node": "7.9.1", "compression": "^1.7.4", "express": "^4.19.2", "get-port": "5.1.1", "morgan": "^1.10.0", "source-map-support": "^0.5.21" }, "peerDependencies": { "react-router": "7.9.1" }, "bin": { "react-router-serve": "bin.js" } }, "sha512-yVBSb5KsNCdkSoOk186/M5GJtcIvKE32Ax9LhXySVpM+suCSjucI+p2TXDOJIYsBqr2aKcBl/bNBm5sIJxG/HA=="],
|
||||
|
||||
"@react-spring/animated": ["@react-spring/animated@9.4.5", "", { "dependencies": { "@react-spring/shared": "~9.4.5", "@react-spring/types": "~9.4.5" }, "peerDependencies": { "react": "^16.8.0 || >=17.0.0 || >=18.0.0" } }, "sha512-KWqrtvJSMx6Fj9nMJkhTwM9r6LIriExDRV6YHZV9HKQsaolUFppgkOXpC+rsL1JEtEvKv6EkLLmSqHTnuYjiIA=="],
|
||||
|
||||
"@react-spring/core": ["@react-spring/core@9.4.5", "", { "dependencies": { "@react-spring/animated": "~9.4.5", "@react-spring/rafz": "~9.4.5", "@react-spring/shared": "~9.4.5", "@react-spring/types": "~9.4.5" }, "peerDependencies": { "react": "^16.8.0 || >=17.0.0 || >=18.0.0" } }, "sha512-83u3FzfQmGMJFwZLAJSwF24/ZJctwUkWtyPD7KYtNagrFeQKUH1I05ZuhmCmqW+2w1KDW1SFWQ43RawqfXKiiQ=="],
|
||||
|
||||
"@react-spring/rafz": ["@react-spring/rafz@9.4.5", "", {}, "sha512-swGsutMwvnoyTRxvqhfJBtGM8Ipx6ks0RkIpNX9F/U7XmyPvBMGd3GgX/mqxZUpdlsuI1zr/jiYw+GXZxAlLcQ=="],
|
||||
|
||||
"@react-spring/shared": ["@react-spring/shared@9.4.5", "", { "dependencies": { "@react-spring/rafz": "~9.4.5", "@react-spring/types": "~9.4.5" }, "peerDependencies": { "react": "^16.8.0 || >=17.0.0 || >=18.0.0" } }, "sha512-JhMh3nFKsqyag0KM5IIM8BQANGscTdd0mMv3BXsUiMZrcjQTskyfnv5qxEeGWbJGGar52qr5kHuBHtCjQOzniA=="],
|
||||
|
||||
"@react-spring/types": ["@react-spring/types@9.4.5", "", {}, "sha512-mpRIamoHwql0ogxEUh9yr4TP0xU5CWyZxVQeccGkHHF8kPMErtDXJlxyo0lj+telRF35XNihtPTWoflqtyARmg=="],
|
||||
|
||||
"@react-spring/web": ["@react-spring/web@9.4.5", "", { "dependencies": { "@react-spring/animated": "~9.4.5", "@react-spring/core": "~9.4.5", "@react-spring/shared": "~9.4.5", "@react-spring/types": "~9.4.5" }, "peerDependencies": { "react": "^16.8.0 || >=17.0.0 || >=18.0.0", "react-dom": "^16.8.0 || >=17.0.0 || >=18.0.0" } }, "sha512-NGAkOtKmOzDEctL7MzRlQGv24sRce++0xAY7KlcxmeVkR7LRSGkoXHaIfm9ObzxPMcPHQYQhf3+X9jepIFNHQA=="],
|
||||
|
||||
"@reduxjs/toolkit": ["@reduxjs/toolkit@2.9.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^10.0.3", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-fSfQlSRu9Z5yBkvsNhYF2rPS8cGXn/TZVrlwN1948QyZ8xMZ0JvP50S2acZNaf+o63u6aEeMjipFyksjIcWrog=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.38", "", {}, "sha512-N/ICGKleNhA5nc9XXQG/kkKHJ7S55u0x0XUJbbkmdCnFuoRkM1Il12q9q0eX19+M7KKUEPw/daUPIRnxhcxAIw=="],
|
||||
|
||||
"@rollup/plugin-alias": ["@rollup/plugin-alias@5.1.1", "", { "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ=="],
|
||||
@ -752,6 +794,10 @@
|
||||
|
||||
"@speed-highlight/core": ["@speed-highlight/core@1.2.7", "", {}, "sha512-0dxmVj4gxg3Jg879kvFS/msl4s9F3T9UXC1InxgOf7t5NvcPD97u/WTA5vL/IxWHMn7qSxBozqrnnE2wvl1m8g=="],
|
||||
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
|
||||
|
||||
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
|
||||
|
||||
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.5", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ=="],
|
||||
|
||||
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@5.1.1", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", "debug": "^4.4.1", "deepmerge": "^4.3.1", "kleur": "^4.1.5", "magic-string": "^0.30.17", "vitefu": "^1.0.6" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.0.0" } }, "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ=="],
|
||||
@ -818,6 +864,32 @@
|
||||
|
||||
"@types/culori": ["@types/culori@4.0.1", "", {}, "sha512-43M51r/22CjhbOXyGT361GZ9vncSVQ39u62x5eJdBQFviI8zWp2X5jzqg7k4M6PVgDQAClpy2bUe2dtwEgEDVQ=="],
|
||||
|
||||
"@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
|
||||
|
||||
"@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="],
|
||||
|
||||
"@types/d3-delaunay": ["@types/d3-delaunay@6.0.4", "", {}, "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw=="],
|
||||
|
||||
"@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="],
|
||||
|
||||
"@types/d3-format": ["@types/d3-format@1.4.5", "", {}, "sha512-mLxrC1MSWupOSncXN/HOlWUAAIffAEBaI4+PKy2uMPsKe4FNZlk7qrbTjmzJXITQQqBHivaks4Td18azgqnotA=="],
|
||||
|
||||
"@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="],
|
||||
|
||||
"@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="],
|
||||
|
||||
"@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="],
|
||||
|
||||
"@types/d3-scale-chromatic": ["@types/d3-scale-chromatic@3.1.0", "", {}, "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ=="],
|
||||
|
||||
"@types/d3-shape": ["@types/d3-shape@3.1.7", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg=="],
|
||||
|
||||
"@types/d3-time": ["@types/d3-time@1.1.4", "", {}, "sha512-JIvy2HjRInE+TXOmIGN5LCmeO0hkFZx5f9FZ7kiN+D+YTcc8pptsiLiuHsvwxwC7VVKmJ2ExHUgNlAiV7vQM9g=="],
|
||||
|
||||
"@types/d3-time-format": ["@types/d3-time-format@2.3.4", "", {}, "sha512-xdDXbpVO74EvadI3UDxjxTdR6QIxm1FKzEA/+F8tL4GWWUg/hgvBqf6chql64U5A9ZUGWo7pEu4eNlyLwbKdhg=="],
|
||||
|
||||
"@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="],
|
||||
|
||||
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
|
||||
|
||||
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
|
||||
@ -850,7 +922,7 @@
|
||||
|
||||
"@types/pg": ["@types/pg@8.15.5", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ=="],
|
||||
|
||||
"@types/react": ["@types/react@19.1.13", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ=="],
|
||||
"@types/react": ["@types/react@19.2.0", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA=="],
|
||||
|
||||
"@types/react-dom": ["@types/react-dom@19.1.9", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ=="],
|
||||
|
||||
@ -860,6 +932,8 @@
|
||||
|
||||
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
||||
|
||||
"@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.45.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.45.0", "@typescript-eslint/type-utils": "8.45.0", "@typescript-eslint/utils": "8.45.0", "@typescript-eslint/visitor-keys": "8.45.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.45.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-HC3y9CVuevvWCl/oyZuI47dOeDF9ztdMEfMH8/DW/Mhwa9cCLnK1oD7JoTVGW/u7kFzNZUKUoyJEqkaJh5y3Wg=="],
|
||||
|
||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.45.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.45.0", "@typescript-eslint/types": "8.45.0", "@typescript-eslint/typescript-estree": "8.45.0", "@typescript-eslint/visitor-keys": "8.45.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-TGf22kon8KW+DeKaUmOibKWktRY8b2NSAZNdtWh798COm1NWx8+xJ6iFBtk3IvLdv6+LGLJLRlyhrhEDZWargQ=="],
|
||||
@ -1084,7 +1158,7 @@
|
||||
|
||||
"bullmq": ["bullmq@5.58.7", "", { "dependencies": { "cron-parser": "^4.9.0", "ioredis": "^5.4.1", "msgpackr": "^1.11.2", "node-abort-controller": "^3.1.1", "semver": "^7.5.4", "tslib": "^2.0.0", "uuid": "^11.1.0" } }, "sha512-rqsKV/ip76wU90q7Cxpr1vS/6PYIVbhuzqr3wgILgjS6XbsnJtWyYrK23jqWHs9+m6/NXM4+62hyf8CSBpufAw=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="],
|
||||
"bun-types": ["bun-types@1.2.23", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-R9f0hKAZXgFU3mlrA0YpE/fiDvwV0FT9rORApt2aQVWSuJDzZOyB5QLc0N/4HF57CS8IXJ6+L5E4W1bW6NS2Aw=="],
|
||||
|
||||
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||
|
||||
@ -1216,6 +1290,32 @@
|
||||
|
||||
"culori": ["culori@4.0.2", "", {}, "sha512-1+BhOB8ahCn4O0cep0Sh2l9KCOfOdY+BXJnKMHFFzDEouSr/el18QwXEMRlOj9UY5nCeA8UN3a/82rUWRBeyBw=="],
|
||||
|
||||
"d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
|
||||
|
||||
"d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="],
|
||||
|
||||
"d3-delaunay": ["d3-delaunay@6.0.4", "", { "dependencies": { "delaunator": "5" } }, "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A=="],
|
||||
|
||||
"d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="],
|
||||
|
||||
"d3-format": ["d3-format@1.4.5", "", {}, "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ=="],
|
||||
|
||||
"d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="],
|
||||
|
||||
"d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="],
|
||||
|
||||
"d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="],
|
||||
|
||||
"d3-scale-chromatic": ["d3-scale-chromatic@3.1.0", "", { "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" } }, "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ=="],
|
||||
|
||||
"d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="],
|
||||
|
||||
"d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="],
|
||||
|
||||
"d3-time-format": ["d3-time-format@3.0.0", "", { "dependencies": { "d3-time": "1 - 2" } }, "sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag=="],
|
||||
|
||||
"d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="],
|
||||
|
||||
"date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="],
|
||||
|
||||
"dax-sh": ["dax-sh@0.43.2", "", { "dependencies": { "@deno/shim-deno": "~0.19.0", "undici-types": "^5.26" } }, "sha512-uULa1sSIHgXKGCqJ/pA0zsnzbHlVnuq7g8O2fkHokWFNwEGIhh5lAJlxZa1POG5En5ba7AU4KcBAvGQWMMf8rg=="],
|
||||
@ -1226,6 +1326,8 @@
|
||||
|
||||
"decamelize": ["decamelize@4.0.0", "", {}, "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ=="],
|
||||
|
||||
"decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="],
|
||||
|
||||
"decode-named-character-reference": ["decode-named-character-reference@1.2.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q=="],
|
||||
|
||||
"dedent": ["dedent@1.7.0", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ=="],
|
||||
@ -1246,6 +1348,8 @@
|
||||
|
||||
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
|
||||
|
||||
"delaunator": ["delaunator@5.0.1", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw=="],
|
||||
|
||||
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
||||
|
||||
"denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
|
||||
@ -1332,6 +1436,8 @@
|
||||
|
||||
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
|
||||
|
||||
"es-toolkit": ["es-toolkit@1.39.10", "", {}, "sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w=="],
|
||||
|
||||
"es6-error": ["es6-error@4.1.1", "", {}, "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="],
|
||||
|
||||
"esbuild": ["esbuild@0.25.10", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.10", "@esbuild/android-arm": "0.25.10", "@esbuild/android-arm64": "0.25.10", "@esbuild/android-x64": "0.25.10", "@esbuild/darwin-arm64": "0.25.10", "@esbuild/darwin-x64": "0.25.10", "@esbuild/freebsd-arm64": "0.25.10", "@esbuild/freebsd-x64": "0.25.10", "@esbuild/linux-arm": "0.25.10", "@esbuild/linux-arm64": "0.25.10", "@esbuild/linux-ia32": "0.25.10", "@esbuild/linux-loong64": "0.25.10", "@esbuild/linux-mips64el": "0.25.10", "@esbuild/linux-ppc64": "0.25.10", "@esbuild/linux-riscv64": "0.25.10", "@esbuild/linux-s390x": "0.25.10", "@esbuild/linux-x64": "0.25.10", "@esbuild/netbsd-arm64": "0.25.10", "@esbuild/netbsd-x64": "0.25.10", "@esbuild/openbsd-arm64": "0.25.10", "@esbuild/openbsd-x64": "0.25.10", "@esbuild/openharmony-arm64": "0.25.10", "@esbuild/sunos-x64": "0.25.10", "@esbuild/win32-arm64": "0.25.10", "@esbuild/win32-ia32": "0.25.10", "@esbuild/win32-x64": "0.25.10" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ=="],
|
||||
@ -1590,6 +1696,8 @@
|
||||
|
||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"immer": ["immer@10.1.3", "", {}, "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw=="],
|
||||
|
||||
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
||||
|
||||
"import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="],
|
||||
@ -1600,6 +1708,8 @@
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
|
||||
|
||||
"ioredis": ["ioredis@5.7.0", "", { "dependencies": { "@ioredis/commands": "^1.3.0", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-NUcA93i1lukyXU+riqEyPtSEkyFq8tX90uL659J+qpCZ3rEdViB/APC58oAhIh3+bJln2hzdlZbBZsGNrlsR8g=="],
|
||||
|
||||
"ip-address": ["ip-address@10.0.1", "", {}, "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA=="],
|
||||
@ -2140,10 +2250,16 @@
|
||||
|
||||
"react-dom": ["react-dom@19.1.1", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.1" } }, "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw=="],
|
||||
|
||||
"react-is": ["react-is@19.2.0", "", {}, "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA=="],
|
||||
|
||||
"react-redux": ["react-redux@9.2.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g=="],
|
||||
|
||||
"react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="],
|
||||
|
||||
"react-router": ["react-router@7.9.1", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-pfAByjcTpX55mqSDGwGnY9vDCpxqBLASg0BMNAuMmpSGESo/TaOUG6BllhAtAkCGx8Rnohik/XtaqiYUJtgW2g=="],
|
||||
|
||||
"react-virtualized-auto-sizer": ["react-virtualized-auto-sizer@1.0.26", "", { "peerDependencies": { "react": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-CblNyiNVw2o+hsa5/49NH2ogGxZ+t+3aweRvNSq7TVjDIlwk7ir4lencEg5HxHeSzwNarSkNkiu0qJSOXtxm5A=="],
|
||||
|
||||
"read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="],
|
||||
|
||||
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||
@ -2154,6 +2270,8 @@
|
||||
|
||||
"recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="],
|
||||
|
||||
"recharts": ["recharts@3.2.1", "", { "dependencies": { "@reduxjs/toolkit": "1.x.x || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-0JKwHRiFZdmLq/6nmilxEZl3pqb4T+aKkOkOi/ZISRZwfBhVMgInxzlYU9D4KnCH3KINScLy68m/OvMXoYGZUw=="],
|
||||
|
||||
"recrawl-sync": ["recrawl-sync@2.2.3", "", { "dependencies": { "@cush/relative": "^1.0.0", "glob-regex": "^0.3.0", "slash": "^3.0.0", "sucrase": "^3.20.3", "tslib": "^1.9.3" } }, "sha512-vSaTR9t+cpxlskkdUFrsEpnf67kSmPk66yAGT1fZPrDudxQjoMzPgQhSMImQ0pAw5k0NPirefQfhopSjhdUtpQ=="],
|
||||
|
||||
"redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="],
|
||||
@ -2162,6 +2280,10 @@
|
||||
|
||||
"redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="],
|
||||
|
||||
"redux": ["redux@5.0.1", "", {}, "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="],
|
||||
|
||||
"redux-thunk": ["redux-thunk@3.1.0", "", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="],
|
||||
|
||||
"regex": ["regex@6.0.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA=="],
|
||||
|
||||
"regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="],
|
||||
@ -2190,6 +2312,8 @@
|
||||
|
||||
"requires-port": ["requires-port@1.0.0", "", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="],
|
||||
|
||||
"reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="],
|
||||
|
||||
"resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="],
|
||||
|
||||
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||
@ -2212,6 +2336,8 @@
|
||||
|
||||
"roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="],
|
||||
|
||||
"robust-predicates": ["robust-predicates@3.0.2", "", {}, "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="],
|
||||
|
||||
"rollup": ["rollup@4.52.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.52.0", "@rollup/rollup-android-arm64": "4.52.0", "@rollup/rollup-darwin-arm64": "4.52.0", "@rollup/rollup-darwin-x64": "4.52.0", "@rollup/rollup-freebsd-arm64": "4.52.0", "@rollup/rollup-freebsd-x64": "4.52.0", "@rollup/rollup-linux-arm-gnueabihf": "4.52.0", "@rollup/rollup-linux-arm-musleabihf": "4.52.0", "@rollup/rollup-linux-arm64-gnu": "4.52.0", "@rollup/rollup-linux-arm64-musl": "4.52.0", "@rollup/rollup-linux-loong64-gnu": "4.52.0", "@rollup/rollup-linux-ppc64-gnu": "4.52.0", "@rollup/rollup-linux-riscv64-gnu": "4.52.0", "@rollup/rollup-linux-riscv64-musl": "4.52.0", "@rollup/rollup-linux-s390x-gnu": "4.52.0", "@rollup/rollup-linux-x64-gnu": "4.52.0", "@rollup/rollup-linux-x64-musl": "4.52.0", "@rollup/rollup-openharmony-arm64": "4.52.0", "@rollup/rollup-win32-arm64-msvc": "4.52.0", "@rollup/rollup-win32-ia32-msvc": "4.52.0", "@rollup/rollup-win32-x64-gnu": "4.52.0", "@rollup/rollup-win32-x64-msvc": "4.52.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-+IuescNkTJQgX7AkIDtITipZdIGcWF0pnVvZTWStiazUmcGA2ag8dfg0urest2XlXUi9kuhfQ+qmdc5Stc3z7g=="],
|
||||
|
||||
"rollup-plugin-visualizer": ["rollup-plugin-visualizer@6.0.3", "", { "dependencies": { "open": "^8.0.0", "picomatch": "^4.0.2", "source-map": "^0.7.4", "yargs": "^17.5.1" }, "peerDependencies": { "rolldown": "1.x || ^1.0.0-beta", "rollup": "2.x || 3.x || 4.x" }, "optionalPeers": ["rolldown", "rollup"], "bin": { "rollup-plugin-visualizer": "dist/bin/cli.js" } }, "sha512-ZU41GwrkDcCpVoffviuM9Clwjy5fcUxlz0oMoTXTYsK+tcIFzbdacnrr2n8TXcHxbGKKXtOdjxM2HUS4HjkwIw=="],
|
||||
@ -2454,7 +2580,7 @@
|
||||
|
||||
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
|
||||
|
||||
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"typescript-eslint": ["typescript-eslint@8.45.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.45.0", "@typescript-eslint/parser": "8.45.0", "@typescript-eslint/typescript-estree": "8.45.0", "@typescript-eslint/utils": "8.45.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qzDmZw/Z5beNLUrXfd0HIW6MzIaAV5WNDxmMs9/3ojGOpYavofgNAAD/nC6tGV2PczIi0iw8vot2eAe/sBn7zg=="],
|
||||
|
||||
@ -2532,6 +2658,8 @@
|
||||
|
||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||
|
||||
"use-debounce": ["use-debounce@10.0.6", "", { "peerDependencies": { "react": "*" } }, "sha512-C5OtPyhAZgVoteO9heXMTdW7v/IbFI+8bSVKYCJrSmiWWCLsbUxiBSp4t9v0hNBTGY97bT72ydDIDyGSFWfwXg=="],
|
||||
|
||||
"use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="],
|
||||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
@ -2556,6 +2684,8 @@
|
||||
|
||||
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
|
||||
|
||||
"victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="],
|
||||
|
||||
"vinxi": ["vinxi@0.5.8", "", { "dependencies": { "@babel/core": "^7.22.11", "@babel/plugin-syntax-jsx": "^7.22.5", "@babel/plugin-syntax-typescript": "^7.22.5", "@types/micromatch": "^4.0.2", "@vinxi/listhen": "^1.5.6", "boxen": "^8.0.1", "chokidar": "^4.0.3", "citty": "^0.1.6", "consola": "^3.4.2", "crossws": "^0.3.4", "dax-sh": "^0.43.0", "defu": "^6.1.4", "es-module-lexer": "^1.7.0", "esbuild": "^0.25.3", "get-port-please": "^3.1.2", "h3": "1.15.3", "hookable": "^5.5.3", "http-proxy": "^1.18.1", "micromatch": "^4.0.8", "nitropack": "^2.11.10", "node-fetch-native": "^1.6.6", "path-to-regexp": "^6.2.1", "pathe": "^1.1.1", "radix3": "^1.1.2", "resolve": "^1.22.10", "serve-placeholder": "^2.0.1", "serve-static": "^1.15.0", "tinyglobby": "^0.2.14", "ufo": "^1.6.1", "unctx": "^2.4.1", "unenv": "^1.10.0", "unstorage": "^1.16.0", "vite": "^6.3.3", "zod": "^3.24.3" }, "bin": { "vinxi": "bin/cli.mjs" } }, "sha512-1pGA+cU1G9feBQ1sd5FMftPuLUT8NSX880AvELhNWqoqWhe2jeSOQxjDPxlA3f1AC+Bbknl4UPKHyVXmfLZQjw=="],
|
||||
|
||||
"vite": ["vite@6.3.6", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA=="],
|
||||
@ -2686,6 +2816,10 @@
|
||||
|
||||
"@koshnic/ratelimit/chai": ["chai@4.5.0", "", { "dependencies": { "assertion-error": "^1.1.0", "check-error": "^1.0.3", "deep-eql": "^4.1.3", "get-func-name": "^2.0.2", "loupe": "^2.3.6", "pathval": "^1.1.1", "type-detect": "^4.1.0" } }, "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw=="],
|
||||
|
||||
"@nivo/scales/@types/d3-time-format": ["@types/d3-time-format@3.0.4", "", {}, "sha512-or9DiDnYI1h38J9hxKEsw513+KVuFbEVhl7qdxcaudoiqWWepapUen+2vAriFGexr6W5+P4l9+HJrB39GG+oRg=="],
|
||||
|
||||
"@nivo/scales/d3-time": ["d3-time@1.1.0", "", {}, "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA=="],
|
||||
|
||||
"@npmcli/git/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="],
|
||||
|
||||
"@npmcli/git/which": ["which@3.0.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg=="],
|
||||
@ -2734,6 +2868,10 @@
|
||||
|
||||
"@tanstack/server-functions-plugin/@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="],
|
||||
|
||||
"@types/bun/bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="],
|
||||
|
||||
"@types/d3-scale/@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
@ -2782,6 +2920,8 @@
|
||||
|
||||
"boxen/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
||||
|
||||
"bun-types/@types/node": ["@types/node@24.6.2", "", { "dependencies": { "undici-types": "~7.13.0" } }, "sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang=="],
|
||||
|
||||
"c12/dotenv": ["dotenv@17.2.2", "", {}, "sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q=="],
|
||||
|
||||
"c12/jiti": ["jiti@2.6.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ=="],
|
||||
@ -2800,6 +2940,8 @@
|
||||
|
||||
"crc32-stream/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="],
|
||||
|
||||
"d3-time-format/d3-time": ["d3-time@2.1.1", "", { "dependencies": { "d3-array": "2" } }, "sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ=="],
|
||||
|
||||
"dax-sh/undici-types": ["undici-types@5.28.4", "", {}, "sha512-3OeMF5Lyowe8VW0skf5qaIE7Or3yS9LS7fvMUI0gg4YxpIBVg0L8BxCmROw2CcYhSkpR68Epz7CGc8MPj94Uww=="],
|
||||
|
||||
"dot-prop/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
||||
@ -2892,10 +3034,6 @@
|
||||
|
||||
"plaette/@types/node": ["@types/node@24.6.2", "", { "dependencies": { "undici-types": "~7.13.0" } }, "sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang=="],
|
||||
|
||||
"plaette/@types/react": ["@types/react@19.2.0", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA=="],
|
||||
|
||||
"plaette/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"plaette/vite": ["vite@7.1.9", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg=="],
|
||||
|
||||
"postcss-nested/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="],
|
||||
@ -2938,6 +3076,8 @@
|
||||
|
||||
"untyped/jiti": ["jiti@2.6.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ=="],
|
||||
|
||||
"victory-vendor/@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="],
|
||||
|
||||
"vinxi/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
|
||||
|
||||
"vinxi/serve-static": ["serve-static@1.16.2", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.19.0" } }, "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw=="],
|
||||
@ -3092,12 +3232,16 @@
|
||||
|
||||
"boxen/string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
|
||||
|
||||
"bun-types/@types/node/undici-types": ["undici-types@7.13.0", "", {}, "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ=="],
|
||||
|
||||
"color/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
|
||||
|
||||
"compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"concurrently/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"d3-time-format/d3-time/d3-array": ["d3-array@2.12.1", "", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="],
|
||||
|
||||
"eslint/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"filelist/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
@ -3186,6 +3330,8 @@
|
||||
|
||||
"boxen/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||
|
||||
"d3-time-format/d3-time/d3-array/internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="],
|
||||
|
||||
"mocha/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"mocha/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
|
||||
@ -1 +1 @@
|
||||
export * from "./snapshots"
|
||||
export * from "./snapshots";
|
||||
|
||||
@ -3,28 +3,28 @@ import { sql } from "drizzle-orm";
|
||||
|
||||
export const getClosestSnapshot = async (aid: number, targetTime: Date) => {
|
||||
const closest = await dbMain.execute<{ created_at: Date; views: number }>(sql`
|
||||
SELECT created_at, views
|
||||
FROM (
|
||||
(SELECT created_at, views, 'later' AS type
|
||||
FROM video_snapshot
|
||||
WHERE aid = ${aid}
|
||||
AND created_at >= ${targetTime.toISOString()}
|
||||
ORDER BY created_at
|
||||
LIMIT 1)
|
||||
UNION ALL
|
||||
(SELECT created_at, views, 'earlier' AS type
|
||||
FROM video_snapshot
|
||||
WHERE aid = ${aid}
|
||||
AND created_at <= ${targetTime.toISOString()}
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1)
|
||||
) AS combined
|
||||
ORDER BY
|
||||
CASE
|
||||
WHEN created_at >= ${targetTime.toISOString()} THEN created_at -${targetTime.toISOString()}
|
||||
ELSE ${targetTime.toISOString()} - created_at
|
||||
END
|
||||
LIMIT 1;
|
||||
`);
|
||||
SELECT created_at, views
|
||||
FROM (
|
||||
(SELECT created_at, views, 'later' AS type
|
||||
FROM video_snapshot
|
||||
WHERE aid = ${aid}
|
||||
AND created_at >= ${targetTime.toISOString()}
|
||||
ORDER BY created_at
|
||||
LIMIT 1)
|
||||
UNION ALL
|
||||
(SELECT created_at, views, 'earlier' AS type
|
||||
FROM video_snapshot
|
||||
WHERE aid = ${aid}
|
||||
AND created_at <= ${targetTime.toISOString()}
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1)
|
||||
) AS combined
|
||||
ORDER BY
|
||||
CASE
|
||||
WHEN created_at >= ${targetTime.toISOString()} THEN created_at -${targetTime.toISOString()}
|
||||
ELSE ${targetTime.toISOString()} - created_at
|
||||
END
|
||||
LIMIT 1;
|
||||
`);
|
||||
return closest[0] || null;
|
||||
};
|
||||
|
||||
@ -2,10 +2,10 @@ import { dbMain } from "@core/drizzle";
|
||||
import { latestVideoSnapshot } from "@core/drizzle/main/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
export const getLatestSnapshot = async (aid: number) =>{
|
||||
export const getLatestSnapshot = async (aid: number) => {
|
||||
const result = await dbMain.select().from(latestVideoSnapshot).where(eq(latestVideoSnapshot.aid, aid)).limit(1);
|
||||
if (result.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return result[0];
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
export * from "./getLatestSnapshot";
|
||||
export * from "./getClosetSnapshot";
|
||||
export * from "./getClosetSnapshot";
|
||||
export * from "./milestone";
|
||||
|
||||
@ -1,30 +1,28 @@
|
||||
import { MINUTE, HOUR, getClosetMilestone, getMileStoneETAfactor, truncate } from "@core/lib";
|
||||
import { MINUTE, HOUR, getClosetMilestone } from "@core/lib";
|
||||
import { getLatestSnapshot, getClosestSnapshot } from "@core/db";
|
||||
|
||||
export const getShortTermETA = async (aid: number) => {
|
||||
export const getShortTermETA = async (aid: number, targetViews?: number): Promise<number> => {
|
||||
const DELTA = 1e-5;
|
||||
let minETAHours = Infinity;
|
||||
const timeIntervals = [20 * MINUTE, HOUR, 3 * HOUR, 6 * HOUR, 24 * HOUR, 72 * HOUR, 168 * HOUR];
|
||||
const currentTimestamp = new Date().getTime();
|
||||
const latestSnapshot = await getLatestSnapshot(aid);
|
||||
const latestSnapshotTime = new Date(latestSnapshot.time).getTime();
|
||||
for (const timeInterval of timeIntervals) {
|
||||
const date = new Date(currentTimestamp - timeInterval);
|
||||
const snapshot = await getClosestSnapshot(aid, date);
|
||||
if (!snapshot) continue;
|
||||
const latestSnapshotTime = new Date(latestSnapshot.time).getTime();
|
||||
const currentSnapshotTime = new Date(snapshot.created_at).getTime();
|
||||
const hoursDiff = (latestSnapshotTime - currentSnapshotTime) / HOUR;
|
||||
const viewsDiff = latestSnapshot.views - snapshot.views;
|
||||
if (viewsDiff <= 0) continue;
|
||||
const speed = viewsDiff / (hoursDiff + DELTA);
|
||||
const target = getClosetMilestone(latestSnapshot.views);
|
||||
const target = targetViews || getClosetMilestone(latestSnapshot.views);
|
||||
const viewsToIncrease = target - latestSnapshot.views;
|
||||
const eta = viewsToIncrease / (speed + DELTA);
|
||||
let factor = getMileStoneETAfactor(viewsToIncrease);
|
||||
factor = truncate(factor, 4.5, 100);
|
||||
const adjustedETA = eta / factor;
|
||||
if (adjustedETA < minETAHours) {
|
||||
minETAHours = adjustedETA;
|
||||
if (eta < minETAHours) {
|
||||
minETAHours = eta;
|
||||
}
|
||||
}
|
||||
return minETAHours;
|
||||
};
|
||||
|
||||
@ -1,22 +1,30 @@
|
||||
import {
|
||||
pgTable,
|
||||
uniqueIndex,
|
||||
index,
|
||||
integer,
|
||||
uniqueIndex,
|
||||
bigserial,
|
||||
bigint,
|
||||
varchar,
|
||||
text,
|
||||
timestamp,
|
||||
smallint,
|
||||
boolean,
|
||||
integer,
|
||||
unique,
|
||||
serial,
|
||||
bigserial,
|
||||
uuid,
|
||||
smallint,
|
||||
boolean,
|
||||
varchar,
|
||||
jsonb,
|
||||
pgSequence
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { sql } from "drizzle-orm";
|
||||
|
||||
export const viewsIncrementRateIdSeq = pgSequence("views_increment_rate_id_seq", {
|
||||
startWith: "1",
|
||||
increment: "1",
|
||||
minValue: "1",
|
||||
maxValue: "9223372036854775807",
|
||||
cache: "1",
|
||||
cycle: false
|
||||
});
|
||||
export const allDataIdSeq = pgSequence("all_data_id_seq", {
|
||||
startWith: "1",
|
||||
increment: "1",
|
||||
@ -49,13 +57,161 @@ export const videoSnapshotIdSeq = pgSequence("video_snapshot_id_seq", {
|
||||
cache: "1",
|
||||
cycle: false
|
||||
});
|
||||
export const viewsIncrementRateIdSeq = pgSequence("views_increment_rate_id_seq", {
|
||||
startWith: "1",
|
||||
increment: "1",
|
||||
minValue: "1",
|
||||
maxValue: "9223372036854775807",
|
||||
cache: "1",
|
||||
cycle: false
|
||||
|
||||
export const snapshotSchedule = pgTable(
|
||||
"snapshot_schedule",
|
||||
{
|
||||
id: bigserial({ mode: "bigint" }).notNull(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
aid: bigint({ mode: "number" }).notNull(),
|
||||
type: text(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true, mode: "string" })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull(),
|
||||
startedAt: timestamp("started_at", { withTimezone: true, mode: "string" }),
|
||||
finishedAt: timestamp("finished_at", { withTimezone: true, mode: "string" }),
|
||||
status: text().default("pending").notNull()
|
||||
},
|
||||
(table) => [
|
||||
index("idx_snapshot_schedule_aid").using("btree", table.aid.asc().nullsLast().op("int8_ops")),
|
||||
index("idx_snapshot_schedule_started_at").using(
|
||||
"btree",
|
||||
table.startedAt.asc().nullsLast().op("timestamptz_ops")
|
||||
),
|
||||
index("idx_snapshot_schedule_status").using("btree", table.status.asc().nullsLast().op("text_ops")),
|
||||
index("idx_snapshot_schedule_type").using("btree", table.type.asc().nullsLast().op("text_ops")),
|
||||
uniqueIndex("snapshot_schedule_pkey").using("btree", table.id.asc().nullsLast().op("int8_ops"))
|
||||
]
|
||||
);
|
||||
|
||||
export const videoSnapshot = pgTable(
|
||||
"video_snapshot",
|
||||
{
|
||||
id: integer()
|
||||
.default(sql`nextval('video_snapshot_id_seq'::regclass)`)
|
||||
.notNull(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true, mode: "string" })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull(),
|
||||
views: integer().notNull(),
|
||||
coins: integer().notNull(),
|
||||
likes: integer().notNull(),
|
||||
favorites: integer().notNull(),
|
||||
shares: integer().notNull(),
|
||||
danmakus: integer().notNull(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
aid: bigint({ mode: "number" }).notNull(),
|
||||
replies: integer().notNull()
|
||||
},
|
||||
(table) => [
|
||||
index("idx_vid_snapshot_aid").using("btree", table.aid.asc().nullsLast().op("int8_ops")),
|
||||
index("idx_vid_snapshot_aid_created_at").using(
|
||||
"btree",
|
||||
table.aid.asc().nullsLast().op("int8_ops"),
|
||||
table.createdAt.asc().nullsLast().op("int8_ops")
|
||||
),
|
||||
index("idx_vid_snapshot_time").using("btree", table.createdAt.asc().nullsLast().op("timestamptz_ops")),
|
||||
index("idx_vid_snapshot_views").using("btree", table.views.asc().nullsLast().op("int4_ops")),
|
||||
uniqueIndex("video_snapshot_pkey").using("btree", table.id.asc().nullsLast().op("int4_ops"))
|
||||
]
|
||||
);
|
||||
|
||||
export const bilibiliUser = pgTable(
|
||||
"bilibili_user",
|
||||
{
|
||||
id: serial().primaryKey().notNull(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
uid: bigint({ mode: "number" }).notNull(),
|
||||
username: text().notNull(),
|
||||
desc: text().notNull(),
|
||||
fans: integer().notNull(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true, mode: "string" })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "string" })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull()
|
||||
},
|
||||
(table) => [
|
||||
index("idx_bili-user_uid").using("btree", table.uid.asc().nullsLast().op("int8_ops")),
|
||||
unique("unq_bili-user_uid").on(table.uid)
|
||||
]
|
||||
);
|
||||
|
||||
export const relations = pgTable(
|
||||
"relations",
|
||||
{
|
||||
id: serial().primaryKey().notNull(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
sourceId: bigint("source_id", { mode: "number" }).notNull(),
|
||||
sourceType: text("source_type").notNull(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
targetId: bigint("target_id", { mode: "number" }).notNull(),
|
||||
targetType: text("target_type").notNull(),
|
||||
relation: text().notNull(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true, mode: "string" })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "string" })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull()
|
||||
},
|
||||
(table) => [
|
||||
index("idx_relations_source_id_source_type_relation").using(
|
||||
"btree",
|
||||
table.sourceId.asc().nullsLast().op("int8_ops"),
|
||||
table.sourceType.asc().nullsLast().op("int8_ops"),
|
||||
table.relation.asc().nullsLast().op("text_ops")
|
||||
),
|
||||
index("idx_relations_target_id_target_type_relation").using(
|
||||
"btree",
|
||||
table.targetId.asc().nullsLast().op("text_ops"),
|
||||
table.targetType.asc().nullsLast().op("text_ops"),
|
||||
table.relation.asc().nullsLast().op("text_ops")
|
||||
),
|
||||
unique("unq_relations").on(table.sourceId, table.sourceType, table.targetId, table.targetType, table.relation)
|
||||
]
|
||||
);
|
||||
|
||||
export const songs = pgTable(
|
||||
"songs",
|
||||
{
|
||||
id: integer()
|
||||
.default(sql`nextval('songs_id_seq'::regclass)`)
|
||||
.notNull(),
|
||||
name: text(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
aid: bigint({ mode: "number" }),
|
||||
publishedAt: timestamp("published_at", { withTimezone: true, mode: "string" }),
|
||||
duration: integer(),
|
||||
type: smallint(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
neteaseId: bigint("netease_id", { mode: "number" }),
|
||||
createdAt: timestamp("created_at", { withTimezone: true, mode: "string" })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "string" })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull(),
|
||||
deleted: boolean().default(false).notNull(),
|
||||
image: text(),
|
||||
producer: text()
|
||||
},
|
||||
(table) => [
|
||||
index("idx_aid").using("btree", table.aid.asc().nullsLast().op("int8_ops")),
|
||||
index("idx_hash_songs_aid").using("hash", table.aid.asc().nullsLast().op("int8_ops")),
|
||||
index("idx_netease_id").using("btree", table.neteaseId.asc().nullsLast().op("int8_ops")),
|
||||
index("idx_published_at").using("btree", table.publishedAt.asc().nullsLast().op("timestamptz_ops")),
|
||||
index("idx_type").using("btree", table.type.asc().nullsLast().op("int2_ops")),
|
||||
uniqueIndex("songs_pkey").using("btree", table.id.asc().nullsLast().op("int4_ops")),
|
||||
uniqueIndex("unq_songs_aid").using("btree", table.aid.asc().nullsLast().op("int8_ops")),
|
||||
uniqueIndex("unq_songs_netease_id").using("btree", table.neteaseId.asc().nullsLast().op("int8_ops"))
|
||||
]
|
||||
);
|
||||
|
||||
export const singer = pgTable("singer", {
|
||||
id: serial().primaryKey().notNull(),
|
||||
name: text().notNull()
|
||||
});
|
||||
|
||||
export const bilibiliMetadata = pgTable(
|
||||
@ -88,6 +244,39 @@ export const bilibiliMetadata = pgTable(
|
||||
]
|
||||
);
|
||||
|
||||
export const humanClassifiedLables = pgTable(
|
||||
"human_classified_lables",
|
||||
{
|
||||
id: serial().notNull(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
aid: bigint({ mode: "number" }).notNull(),
|
||||
uid: integer().notNull(),
|
||||
label: smallint().notNull(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true, mode: "string" })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull()
|
||||
},
|
||||
(table) => [
|
||||
index("idx_classified-labels-human_aid").using("btree", table.aid.asc().nullsLast().op("int8_ops")),
|
||||
index("idx_classified-labels-human_author").using("btree", table.uid.asc().nullsLast().op("int4_ops")),
|
||||
index("idx_classified-labels-human_created-at").using(
|
||||
"btree",
|
||||
table.createdAt.asc().nullsLast().op("timestamptz_ops")
|
||||
),
|
||||
index("idx_classified-labels-human_label").using("btree", table.label.asc().nullsLast().op("int2_ops"))
|
||||
]
|
||||
);
|
||||
|
||||
export const history = pgTable("history", {
|
||||
id: serial().primaryKey().notNull(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
objectId: bigint("object_id", { mode: "number" }).notNull(),
|
||||
changeType: text("change_type").notNull(),
|
||||
changedAt: timestamp("changed_at", { withTimezone: true, mode: "string" }).notNull(),
|
||||
changedBy: integer("changed_by").notNull(),
|
||||
data: jsonb().notNull()
|
||||
});
|
||||
|
||||
export const labellingResult = pgTable(
|
||||
"labelling_result",
|
||||
{
|
||||
@ -143,187 +332,3 @@ export const latestVideoSnapshot = pgTable(
|
||||
index("idx_latest-video-snapshot_views").using("btree", table.views.asc().nullsLast().op("int4_ops"))
|
||||
]
|
||||
);
|
||||
|
||||
export const videoSnapshot = pgTable(
|
||||
"video_snapshot",
|
||||
{
|
||||
id: integer()
|
||||
.default(sql`nextval('video_snapshot_id_seq'::regclass)`)
|
||||
.notNull(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true, mode: "string" })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull(),
|
||||
views: integer().notNull(),
|
||||
coins: integer().notNull(),
|
||||
likes: integer().notNull(),
|
||||
favorites: integer().notNull(),
|
||||
shares: integer().notNull(),
|
||||
danmakus: integer().notNull(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
aid: bigint({ mode: "number" }).notNull(),
|
||||
replies: integer().notNull()
|
||||
},
|
||||
(table) => [
|
||||
index("idx_vid_snapshot_aid").using("btree", table.aid.asc().nullsLast().op("int8_ops")),
|
||||
index("idx_vid_snapshot_aid_created_at").using(
|
||||
"btree",
|
||||
table.aid.asc().nullsLast().op("timestamptz_ops"),
|
||||
table.createdAt.asc().nullsLast().op("timestamptz_ops")
|
||||
),
|
||||
index("idx_vid_snapshot_time").using("btree", table.createdAt.asc().nullsLast().op("timestamptz_ops")),
|
||||
index("idx_vid_snapshot_views").using("btree", table.views.asc().nullsLast().op("int4_ops")),
|
||||
uniqueIndex("video_snapshot_pkey").using("btree", table.id.asc().nullsLast().op("int4_ops"))
|
||||
]
|
||||
);
|
||||
|
||||
export const songs = pgTable(
|
||||
"songs",
|
||||
{
|
||||
id: integer()
|
||||
.default(sql`nextval('songs_id_seq'::regclass)`)
|
||||
.notNull(),
|
||||
name: text(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
aid: bigint({ mode: "number" }),
|
||||
publishedAt: timestamp("published_at", { withTimezone: true, mode: "string" }),
|
||||
duration: integer(),
|
||||
type: smallint(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
neteaseId: bigint("netease_id", { mode: "number" }),
|
||||
createdAt: timestamp("created_at", { withTimezone: true, mode: "string" })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "string" })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull(),
|
||||
deleted: boolean().default(false).notNull(),
|
||||
image: text(),
|
||||
producer: text()
|
||||
},
|
||||
(table) => [
|
||||
index("idx_aid").using("btree", table.aid.asc().nullsLast().op("int8_ops")),
|
||||
index("idx_hash_songs_aid").using("hash", table.aid.asc().nullsLast().op("int8_ops")),
|
||||
index("idx_netease_id").using("btree", table.neteaseId.asc().nullsLast().op("int8_ops")),
|
||||
index("idx_published_at").using("btree", table.publishedAt.asc().nullsLast().op("timestamptz_ops")),
|
||||
index("idx_type").using("btree", table.type.asc().nullsLast().op("int2_ops")),
|
||||
uniqueIndex("songs_pkey").using("btree", table.id.asc().nullsLast().op("int4_ops")),
|
||||
uniqueIndex("unq_songs_aid").using("btree", table.aid.asc().nullsLast().op("int8_ops")),
|
||||
uniqueIndex("unq_songs_netease_id").using("btree", table.neteaseId.asc().nullsLast().op("int8_ops"))
|
||||
]
|
||||
);
|
||||
|
||||
export const bilibiliUser = pgTable(
|
||||
"bilibili_user",
|
||||
{
|
||||
id: serial().primaryKey().notNull(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
uid: bigint({ mode: "number" }).notNull(),
|
||||
username: text().notNull(),
|
||||
desc: text().notNull(),
|
||||
fans: integer().notNull(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true, mode: "string" })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "string" })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull()
|
||||
},
|
||||
(table) => [
|
||||
index("idx_bili-user_uid").using("btree", table.uid.asc().nullsLast().op("int8_ops")),
|
||||
unique("unq_bili-user_uid").on(table.uid)
|
||||
]
|
||||
);
|
||||
|
||||
export const singer = pgTable("singer", {
|
||||
id: serial().primaryKey().notNull(),
|
||||
name: text().notNull()
|
||||
});
|
||||
|
||||
export const relations = pgTable(
|
||||
"relations",
|
||||
{
|
||||
id: serial().primaryKey().notNull(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
sourceId: bigint("source_id", { mode: "number" }).notNull(),
|
||||
sourceType: text("source_type").notNull(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
targetId: bigint("target_id", { mode: "number" }).notNull(),
|
||||
targetType: text("target_type").notNull(),
|
||||
relation: text().notNull(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true, mode: "string" })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "string" })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull()
|
||||
},
|
||||
(table) => [
|
||||
index("idx_relations_source_id_source_type_relation").using(
|
||||
"btree",
|
||||
table.sourceId.asc().nullsLast().op("int8_ops"),
|
||||
table.sourceType.asc().nullsLast().op("int8_ops"),
|
||||
table.relation.asc().nullsLast().op("text_ops")
|
||||
),
|
||||
index("idx_relations_target_id_target_type_relation").using(
|
||||
"btree",
|
||||
table.targetId.asc().nullsLast().op("text_ops"),
|
||||
table.targetType.asc().nullsLast().op("text_ops"),
|
||||
table.relation.asc().nullsLast().op("text_ops")
|
||||
),
|
||||
unique("unq_relations").on(table.sourceId, table.sourceType, table.targetId, table.targetType, table.relation)
|
||||
]
|
||||
);
|
||||
|
||||
export const globalKv = pgTable("global_kv", {
|
||||
key: text().primaryKey().notNull(),
|
||||
value: text().notNull()
|
||||
});
|
||||
|
||||
export const snapshotSchedule = pgTable(
|
||||
"snapshot_schedule",
|
||||
{
|
||||
id: bigserial({ mode: "bigint" }).notNull(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
aid: bigint({ mode: "number" }).notNull(),
|
||||
type: text(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true, mode: "string" })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull(),
|
||||
startedAt: timestamp("started_at", { withTimezone: true, mode: "string" }),
|
||||
finishedAt: timestamp("finished_at", { withTimezone: true, mode: "string" }),
|
||||
status: text().default("pending").notNull()
|
||||
},
|
||||
(table) => [
|
||||
index("idx_snapshot_schedule_aid").using("btree", table.aid.asc().nullsLast().op("int8_ops")),
|
||||
index("idx_snapshot_schedule_started_at").using(
|
||||
"btree",
|
||||
table.startedAt.asc().nullsLast().op("timestamptz_ops")
|
||||
),
|
||||
index("idx_snapshot_schedule_status").using("btree", table.status.asc().nullsLast().op("text_ops")),
|
||||
index("idx_snapshot_schedule_type").using("btree", table.type.asc().nullsLast().op("text_ops")),
|
||||
uniqueIndex("snapshot_schedule_pkey").using("btree", table.id.asc().nullsLast().op("int8_ops"))
|
||||
]
|
||||
);
|
||||
|
||||
export const classifiedLabelsHuman = pgTable(
|
||||
"classified_labels_human",
|
||||
{
|
||||
id: serial().primaryKey().notNull(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
aid: bigint({ mode: "number" }).notNull(),
|
||||
author: uuid().notNull(),
|
||||
label: smallint().notNull(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true, mode: "string" })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull()
|
||||
},
|
||||
(table) => [
|
||||
index("idx_classified-labels-human_aid").using("btree", table.aid.asc().nullsLast().op("int8_ops")),
|
||||
index("idx_classified-labels-human_author").using("btree", table.author.asc().nullsLast().op("uuid_ops")),
|
||||
index("idx_classified-labels-human_created-at").using(
|
||||
"btree",
|
||||
table.createdAt.asc().nullsLast().op("timestamptz_ops")
|
||||
),
|
||||
index("idx_classified-labels-human_label").using("btree", table.label.asc().nullsLast().op("int2_ops"))
|
||||
]
|
||||
);
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
"dependencies": {
|
||||
"@elysiajs/cors": "^1.4.0",
|
||||
"@elysiajs/openapi": "^1.4.0",
|
||||
"@elysiajs/server-timing": "^1.4.0",
|
||||
"chalk": "^5.6.2",
|
||||
"elysia": "^1.4.0",
|
||||
"zod": "^4.1.11"
|
||||
|
||||
@ -2,13 +2,14 @@ import { Elysia, t } from "elysia";
|
||||
import { dbMain } from "@core/drizzle";
|
||||
import { bilibiliMetadata, latestVideoSnapshot } from "@core/drizzle/main/schema";
|
||||
import { eq, and, gte, lt, desc } from "drizzle-orm";
|
||||
import { getShortTermETA } from "@core/db";
|
||||
|
||||
type MileStoneType = "dendou" | "densetsu" | "shinwa";
|
||||
|
||||
const range = {
|
||||
dendou: [90000, 99999],
|
||||
densetsu: [900000, 999999],
|
||||
shinwa: [5000000, 9999999]
|
||||
dendou: [90000, 99999, 100000],
|
||||
densetsu: [900000, 999999, 1000000],
|
||||
shinwa: [5000000, 9999999, 10000000]
|
||||
};
|
||||
|
||||
export const closeMileStoneHandler = new Elysia({ prefix: "/song" }).get(
|
||||
@ -23,12 +24,21 @@ export const closeMileStoneHandler = new Elysia({ prefix: "/song" }).get(
|
||||
.innerJoin(latestVideoSnapshot, eq(latestVideoSnapshot.aid, bilibiliMetadata.aid))
|
||||
.where(and(gte(latestVideoSnapshot.views, min), lt(latestVideoSnapshot.views, max)))
|
||||
.orderBy(desc(latestVideoSnapshot.views));
|
||||
const aids = data.map((song) => song.bilibili_metadata.aid);
|
||||
for (const aid of aids) {
|
||||
|
||||
}
|
||||
|
||||
return data;
|
||||
type Row = (typeof data)[number];
|
||||
type Result = Row & {
|
||||
eta: number;
|
||||
};
|
||||
const result: Result[] = [];
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const aid = data[i].bilibili_metadata.aid;
|
||||
const eta = await getShortTermETA(aid, range[type as MileStoneType][2]);
|
||||
result.push({
|
||||
...data[i],
|
||||
eta
|
||||
});
|
||||
}
|
||||
result.sort((a, b) => a.eta - b.eta);
|
||||
return result;
|
||||
},
|
||||
{
|
||||
response: {
|
||||
|
||||
@ -6,6 +6,8 @@ import { cors } from "@elysiajs/cors";
|
||||
import { getSongInfoHandler } from "@elysia/routes/song/info";
|
||||
import { rootHandler } from "@elysia/routes/root";
|
||||
import { getVideoMetadataHandler } from "@elysia/routes/video/metadata";
|
||||
import { closeMileStoneHandler } from "@elysia/routes/song/milestone";
|
||||
import { serverTiming } from '@elysiajs/server-timing'
|
||||
|
||||
const [host, port] = getBindingInfo();
|
||||
logStartup(host, port);
|
||||
@ -15,12 +17,14 @@ const app = new Elysia({
|
||||
hostname: host
|
||||
}
|
||||
})
|
||||
.use(serverTiming())
|
||||
.use(cors())
|
||||
.use(openapi())
|
||||
.use(rootHandler)
|
||||
.use(pingHandler)
|
||||
.use(getVideoMetadataHandler)
|
||||
.use(getSongInfoHandler)
|
||||
.use(closeMileStoneHandler)
|
||||
.listen(15412);
|
||||
|
||||
export const VERSION = "0.7.0";
|
||||
|
||||
@ -71,6 +71,7 @@ export const Slider = ({ useP3, channel, color, onChange, i18nProvider }: Slider
|
||||
};
|
||||
|
||||
const handleTouchMove = (e: React.TouchEvent) => {
|
||||
e.preventDefault();
|
||||
const touch = e.touches[0];
|
||||
if (touch) {
|
||||
const newValue = getValueFromPosition(touch.clientX);
|
||||
|
||||
480
packages/temp_frontend/app/components/Chart.tsx
Normal file
480
packages/temp_frontend/app/components/Chart.tsx
Normal file
@ -0,0 +1,480 @@
|
||||
import { useState, useRef, useMemo, useCallback, useEffect } from "react";
|
||||
|
||||
export const TimeSeriesChart = ({
|
||||
data = [],
|
||||
width = "100%",
|
||||
height = 300,
|
||||
accentColor = "#007AFF",
|
||||
showGrid = true,
|
||||
smoothInterpolation = true,
|
||||
timeRange = "auto", // '6h', '1d', '7d', '30d', 'auto'
|
||||
}: {
|
||||
data: { timestamp: number; value: number }[];
|
||||
width?: string;
|
||||
height?: number;
|
||||
accentColor?: string;
|
||||
showGrid?: boolean;
|
||||
smoothInterpolation?: boolean;
|
||||
timeRange?: "6h" | "1d" | "7d" | "30d" | "auto";
|
||||
}) => {
|
||||
const svgRef = useRef<SVGSVGElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [currentPosition, setCurrentPosition] = useState<{
|
||||
x: number;
|
||||
y: number;
|
||||
data: { timestamp: number; value: number } | null;
|
||||
} | null>(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [dragStartX, setDragStartX] = useState(0);
|
||||
const [viewBox, setViewBox] = useState({ start: 0, end: 1 });
|
||||
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
|
||||
const [isTouchActive, setIsTouchActive] = useState(false);
|
||||
const [longPressTimer, setLongPressTimer] = useState<NodeJS.Timeout | null>(null);
|
||||
const [touchStartPosition, setTouchStartPosition] = useState<{ x: number; y: number } | null>(null);
|
||||
|
||||
// 响应式尺寸处理
|
||||
useEffect(() => {
|
||||
const updateDimensions = () => {
|
||||
if (containerRef.current) {
|
||||
const { width: containerWidth, height: containerHeight } = containerRef.current.getBoundingClientRect();
|
||||
setDimensions({ width: containerWidth, height: containerHeight });
|
||||
}
|
||||
};
|
||||
|
||||
updateDimensions();
|
||||
window.addEventListener("resize", updateDimensions);
|
||||
return () => window.removeEventListener("resize", updateDimensions);
|
||||
}, []);
|
||||
|
||||
// 格式化时间标签
|
||||
const formatTimeLabel = useCallback((timestamp: number, range: "6h" | "1d" | "7d" | "30d" | "auto") => {
|
||||
const date = new Date(timestamp);
|
||||
const now = new Date();
|
||||
const isToday = date.toDateString() === now.toDateString();
|
||||
|
||||
switch (range) {
|
||||
case "6h":
|
||||
return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
||||
case "1d":
|
||||
return isToday ? "今天" : date.toLocaleDateString([], { month: "short", day: "numeric" });
|
||||
case "7d":
|
||||
return date.toLocaleDateString([], { weekday: "short" });
|
||||
case "30d":
|
||||
return date.toLocaleDateString([], { month: "short", day: "numeric" });
|
||||
default:
|
||||
return date.toLocaleDateString([], { month: "short", day: "numeric" });
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 处理数据范围和时间间隔
|
||||
const { xScale, yScale, visibleData, timeTicks, yTicks } = useMemo(() => {
|
||||
if (!data.length || !dimensions.width) return {};
|
||||
|
||||
const visibleDataPoints = data.filter((point, index) => {
|
||||
const progress = index / (data.length - 1);
|
||||
return progress >= viewBox.start && progress <= viewBox.end;
|
||||
});
|
||||
|
||||
if (!visibleDataPoints.length) return {};
|
||||
|
||||
// 计算Y轴范围(带一些边距)
|
||||
const values = visibleDataPoints.map((d) => d.value);
|
||||
const minValue = Math.min(...values);
|
||||
const maxValue = Math.max(...values);
|
||||
const valueRange = maxValue - minValue;
|
||||
const padding = valueRange * 0.1;
|
||||
|
||||
const yScale = (value: number) => {
|
||||
const chartHeight = dimensions.height - 60; // 为标签留出空间
|
||||
return chartHeight - ((value - (minValue - padding)) / (maxValue - minValue + 2 * padding)) * chartHeight + 20;
|
||||
};
|
||||
|
||||
// X轴比例尺
|
||||
const xScale = (index: number) => {
|
||||
const totalPoints = visibleDataPoints.length;
|
||||
return (index / (totalPoints - 1)) * (dimensions.width - 60) + 40;
|
||||
};
|
||||
|
||||
// 生成时间刻度
|
||||
const generateTimeTicks = () => {
|
||||
const tickCount = Math.min(6, Math.floor(dimensions.width / 80));
|
||||
const ticks = [];
|
||||
|
||||
for (let i = 0; i < tickCount; i++) {
|
||||
const dataIndex = Math.floor((i / (tickCount - 1)) * (visibleDataPoints.length - 1));
|
||||
if (visibleDataPoints[dataIndex]) {
|
||||
ticks.push({
|
||||
x: xScale(dataIndex),
|
||||
timestamp: visibleDataPoints[dataIndex].timestamp,
|
||||
label: formatTimeLabel(visibleDataPoints[dataIndex].timestamp, timeRange),
|
||||
});
|
||||
}
|
||||
}
|
||||
return ticks;
|
||||
};
|
||||
|
||||
// 生成Y轴刻度
|
||||
const generateYTicks = () => {
|
||||
const tickCount = 4;
|
||||
const ticks = [];
|
||||
|
||||
for (let i = 0; i <= tickCount; i++) {
|
||||
const value = minValue - padding + (maxValue + padding - (minValue - padding)) * (i / tickCount);
|
||||
const y = yScale(value);
|
||||
ticks.push({
|
||||
y,
|
||||
value: Math.round(value * 100) / 100, // 保留两位小数
|
||||
});
|
||||
}
|
||||
return ticks;
|
||||
};
|
||||
|
||||
return {
|
||||
xScale,
|
||||
yScale,
|
||||
visibleData: visibleDataPoints,
|
||||
timeTicks: generateTimeTicks(),
|
||||
yTicks: generateYTicks(),
|
||||
};
|
||||
}, [data, dimensions, viewBox, timeRange]);
|
||||
|
||||
// 生成平滑路径
|
||||
const generatePath = useCallback(() => {
|
||||
if (!visibleData || !xScale || !yScale) return "";
|
||||
|
||||
if (!smoothInterpolation || visibleData.length < 3) {
|
||||
// 直线连接
|
||||
return visibleData
|
||||
.map((point, index) => `${index === 0 ? "M" : "L"} ${xScale(index)} ${yScale(point.value)}`)
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
// Catmull-Rom 平滑曲线
|
||||
const points = visibleData.map((point, index) => ({
|
||||
x: xScale(index),
|
||||
y: yScale(point.value),
|
||||
}));
|
||||
|
||||
let path = `M ${points[0].x} ${points[0].y}`;
|
||||
|
||||
for (let i = 0; i < points.length - 1; i++) {
|
||||
const p0 = points[Math.max(0, i - 1)];
|
||||
const p1 = points[i];
|
||||
const p2 = points[i + 1];
|
||||
const p3 = points[Math.min(points.length - 1, i + 2)];
|
||||
|
||||
const tension = 0.5;
|
||||
const x1 = p1.x + ((p2.x - p0.x) / 6) * tension;
|
||||
const y1 = p1.y + ((p2.y - p0.y) / 6) * tension;
|
||||
const x2 = p2.x - ((p3.x - p1.x) / 6) * tension;
|
||||
const y2 = p2.y - ((p3.y - p1.y) / 6) * tension;
|
||||
|
||||
path += ` C ${x1} ${y1} ${x2} ${y2} ${p2.x} ${p2.y}`;
|
||||
}
|
||||
|
||||
return path;
|
||||
}, [visibleData, xScale, yScale, smoothInterpolation]);
|
||||
|
||||
// 更新光标位置和对应数据点
|
||||
const updateCursorPosition = useCallback(
|
||||
(x: number) => {
|
||||
if (!visibleData || !xScale) return;
|
||||
|
||||
// 找到最近的数据点
|
||||
let closestIndex = 0;
|
||||
let minDistance = Infinity;
|
||||
|
||||
visibleData.forEach((point, index) => {
|
||||
const pointX = xScale(index);
|
||||
const distance = Math.abs(pointX - x);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
closestIndex = index;
|
||||
}
|
||||
});
|
||||
|
||||
if (minDistance < 50) {
|
||||
// 灵敏度阈值
|
||||
const point = visibleData[closestIndex];
|
||||
setCurrentPosition({
|
||||
x: xScale(closestIndex),
|
||||
y: yScale(point.value),
|
||||
data: point,
|
||||
});
|
||||
} else {
|
||||
setCurrentPosition(null);
|
||||
}
|
||||
},
|
||||
[visibleData, xScale, yScale],
|
||||
);
|
||||
|
||||
// 鼠标事件处理
|
||||
const handleMouseDown = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
console.log("mouse down");
|
||||
if (!svgRef.current) return;
|
||||
|
||||
const rect = svgRef.current.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
|
||||
setIsDragging(true);
|
||||
setDragStartX(x);
|
||||
updateCursorPosition(x);
|
||||
},
|
||||
[updateCursorPosition],
|
||||
);
|
||||
|
||||
const handleMouseMove = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
console.log("mouse move");
|
||||
if (!svgRef.current) return;
|
||||
|
||||
const rect = svgRef.current.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
|
||||
if (isDragging) {
|
||||
// 滚动逻辑
|
||||
const deltaX = x - dragStartX;
|
||||
if (Math.abs(deltaX) > 10) {
|
||||
const dragSpeed = 0.02;
|
||||
const newStart = Math.max(0, viewBox.start - deltaX * dragSpeed);
|
||||
const newEnd = Math.min(1, viewBox.end - deltaX * dragSpeed);
|
||||
|
||||
if (newEnd - newStart === viewBox.end - viewBox.start) {
|
||||
setViewBox({ start: newStart, end: newEnd });
|
||||
}
|
||||
setDragStartX(x);
|
||||
} else {
|
||||
// 光标位置更新
|
||||
updateCursorPosition(x);
|
||||
}
|
||||
} else {
|
||||
// 悬停时更新光标位置
|
||||
updateCursorPosition(x);
|
||||
}
|
||||
},
|
||||
[isDragging, dragStartX, viewBox, updateCursorPosition],
|
||||
);
|
||||
|
||||
const handleMouseUp = useCallback(() => {
|
||||
console.log("mouse up");
|
||||
setIsDragging(false);
|
||||
}, []);
|
||||
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
console.log("mouse leave");
|
||||
setIsDragging(false);
|
||||
setCurrentPosition(null);
|
||||
}, []);
|
||||
|
||||
// 触摸事件处理
|
||||
const handleTouchStart = useCallback(
|
||||
(e: React.TouchEvent) => {
|
||||
console.log("touch start");
|
||||
if (!svgRef.current) return;
|
||||
|
||||
const touch = e.touches[0];
|
||||
const rect = svgRef.current.getBoundingClientRect();
|
||||
const x = touch.clientX - rect.left;
|
||||
|
||||
setIsDragging(true);
|
||||
setDragStartX(x);
|
||||
updateCursorPosition(x);
|
||||
},
|
||||
[updateCursorPosition],
|
||||
);
|
||||
|
||||
const handleTouchMove = useCallback(
|
||||
(e: React.TouchEvent) => {
|
||||
console.log("touch move");
|
||||
if (!isDragging || !svgRef.current) return;
|
||||
|
||||
const touch = e.touches[0];
|
||||
const rect = svgRef.current.getBoundingClientRect();
|
||||
const x = touch.clientX - rect.left;
|
||||
|
||||
// 滚动逻辑
|
||||
const deltaX = x - dragStartX;
|
||||
if (Math.abs(deltaX) > 10) {
|
||||
const dragSpeed = 0.02;
|
||||
const newStart = Math.max(0, viewBox.start - deltaX * dragSpeed);
|
||||
const newEnd = Math.min(1, viewBox.end - deltaX * dragSpeed);
|
||||
|
||||
if (newEnd - newStart === viewBox.end - viewBox.start) {
|
||||
setViewBox({ start: newStart, end: newEnd });
|
||||
}
|
||||
setDragStartX(x);
|
||||
} else {
|
||||
// 光标位置更新
|
||||
updateCursorPosition(x);
|
||||
}
|
||||
},
|
||||
[isDragging, dragStartX, viewBox, updateCursorPosition],
|
||||
);
|
||||
|
||||
const handleTouchEnd = useCallback(() => {
|
||||
console.log("touch end");
|
||||
setIsDragging(false);
|
||||
setCurrentPosition(null);
|
||||
}, []);
|
||||
|
||||
|
||||
if (!data.length) {
|
||||
return (
|
||||
<div ref={containerRef} className="time-series-chart" style={{ width, height, position: "relative" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "100%",
|
||||
color: "#999",
|
||||
fontSize: "14px",
|
||||
}}
|
||||
>
|
||||
暂无数据
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="time-series-chart"
|
||||
style={{
|
||||
width,
|
||||
height,
|
||||
position: "relative",
|
||||
touchAction: "none",
|
||||
userSelect: "none",
|
||||
WebkitUserSelect: "none",
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
ref={svgRef}
|
||||
width={dimensions.width}
|
||||
height={dimensions.height}
|
||||
style={{ display: "block" }}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseUp={handleMouseUp}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
>
|
||||
|
||||
{/* Y轴刻度 */}
|
||||
{yTicks &&
|
||||
yTicks.map((tick, index) => (
|
||||
<g key={`y-${index}`}>
|
||||
<line
|
||||
x1={40}
|
||||
y1={tick.y}
|
||||
x2={dimensions.width - 20}
|
||||
y2={tick.y}
|
||||
stroke="rgba(200, 200, 200, 0.3)"
|
||||
strokeWidth="1"
|
||||
strokeDasharray="2,2"
|
||||
/>
|
||||
<text
|
||||
x={35}
|
||||
y={tick.y + 4}
|
||||
textAnchor="end"
|
||||
className="text-xs fill-gray-400 dark:fill-neutral-300"
|
||||
>
|
||||
{tick.value.toLocaleString()}
|
||||
</text>
|
||||
</g>
|
||||
))}
|
||||
|
||||
{/* X轴时间刻度 */}
|
||||
{timeTicks &&
|
||||
timeTicks.map((tick, index) => (
|
||||
<g key={`x-${index}`}>
|
||||
<line
|
||||
x1={tick.x}
|
||||
y1={20}
|
||||
x2={tick.x}
|
||||
y2={dimensions.height - 40}
|
||||
stroke="rgba(200, 200, 200, 0.3)"
|
||||
strokeWidth="1"
|
||||
strokeDasharray="2,2"
|
||||
/>
|
||||
<text
|
||||
x={tick.x}
|
||||
y={dimensions.height - 8}
|
||||
textAnchor="middle"
|
||||
className="text-xs fill-gray-400 dark:fill-neutral-300"
|
||||
>
|
||||
{tick.label}
|
||||
</text>
|
||||
</g>
|
||||
))}
|
||||
|
||||
{/* 折线路径 */}
|
||||
<path d={generatePath()} fill="none" stroke={accentColor} strokeWidth="2" strokeLinecap="round" />
|
||||
|
||||
{/* 当前光标指示线 */}
|
||||
{currentPosition && (
|
||||
<g>
|
||||
{/* 垂直指示线 */}
|
||||
<line
|
||||
x1={currentPosition.x}
|
||||
y1={0}
|
||||
x2={currentPosition.x}
|
||||
y2={dimensions.height - 25}
|
||||
strokeWidth="1"
|
||||
className="stroke-gray-500 dark:stroke-neutral-700"
|
||||
/>
|
||||
{/* 数据点 */}
|
||||
<circle
|
||||
cx={currentPosition.x}
|
||||
cy={currentPosition.y}
|
||||
r="4"
|
||||
fill={accentColor}
|
||||
stroke="white"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</g>
|
||||
)}
|
||||
</svg>
|
||||
|
||||
{/* 浮动数据标签 */}
|
||||
{currentPosition && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 10,
|
||||
left: 20,
|
||||
right: 20,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<div className="bg-white/90 dark:bg-neutral-800 text-sm rounded-md px-2 py-1">
|
||||
{currentPosition.data &&
|
||||
new Date(currentPosition.data.timestamp).toLocaleTimeString([], {
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="bg-white/90 dark:bg-neutral-800 text-sm rounded-md px-2 py-1">
|
||||
{currentPosition.data && currentPosition.data.value.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,3 +1,8 @@
|
||||
import { type RouteConfig, index, route } from "@react-router/dev/routes";
|
||||
|
||||
export default [index("routes/home.tsx"), route("song/:id/info", "routes/song/[id]/info.tsx")] satisfies RouteConfig;
|
||||
export default [
|
||||
index("routes/home.tsx"),
|
||||
route("song/:id/info", "routes/song/[id]/info.tsx"),
|
||||
route("song/:id/data", "routes/song/[id]/data.tsx"),
|
||||
route("chart-demo", "routes/chartDemo.tsx"),
|
||||
] satisfies RouteConfig;
|
||||
|
||||
59
packages/temp_frontend/app/routes/chartDemo.tsx
Normal file
59
packages/temp_frontend/app/routes/chartDemo.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import { TimeSeriesChart } from "@/components/Chart";
|
||||
import { useEffect, useState } from "react";
|
||||
import useSWR from "swr";
|
||||
|
||||
const API_URL = "https://api.projectcvsa.com";
|
||||
|
||||
const App = () => {
|
||||
const { data, error, isLoading } = useSWR(`${API_URL}/video/av285205499/snapshots`, async (url) => {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch song info");
|
||||
}
|
||||
return response.json();
|
||||
});
|
||||
|
||||
function generateSampleData() {
|
||||
if (!data || data.length === 0) return [];
|
||||
const d = [];
|
||||
for (let i = data.length - 1; i >= 0; i--) {
|
||||
d.push({
|
||||
timestamp: data[i].created_at,
|
||||
value: data[i].views
|
||||
});
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
const [chartData, setChartData] = useState(generateSampleData());
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
setChartData(generateSampleData());
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: "20px",
|
||||
maxWidth: "500px",
|
||||
margin: "0 auto",
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
||||
}}
|
||||
>
|
||||
<h2 className="mb-4">健康数据趋势</h2>
|
||||
|
||||
<TimeSeriesChart
|
||||
data={chartData}
|
||||
height={280}
|
||||
accentColor="#007AFF"
|
||||
smoothInterpolation={true}
|
||||
timeRange="30d"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
@ -10,6 +10,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@elysiajs/eden": "^1.4.1",
|
||||
"@nivo/core": "^0.99.0",
|
||||
"@nivo/line": "^0.99.0",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@react-router/node": "^7.7.1",
|
||||
"@react-router/serve": "^7.7.1",
|
||||
@ -21,6 +23,7 @@
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-router": "^7.7.1",
|
||||
"recharts": "^3.2.1",
|
||||
"sonner": "^2.0.7",
|
||||
"swr": "^2.3.6",
|
||||
"tailwind-merge": "^3.3.1"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user