diff --git a/bun.lock b/bun.lock index 2c54530..bab41b6 100644 --- a/bun.lock +++ b/bun.lock @@ -92,7 +92,7 @@ "chalk": "^5.6.2", "elysia": "^1.4.0", "elysia-ip": "^1.0.10", - "zod": "^4.1.11", + "zod": "^4.1.12", }, "devDependencies": { "bun-types": "latest", @@ -199,6 +199,7 @@ "@elysiajs/eden": "^1.4.1", "@nivo/core": "^0.99.0", "@nivo/line": "^0.99.0", + "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tabs": "^1.1.13", "@react-router/node": "^7.7.1", @@ -758,10 +759,20 @@ "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="], + "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], + "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], + + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="], + + "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], + "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], + "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], + "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], @@ -778,6 +789,8 @@ "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="], + "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="], + "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], "@react-router/dev": ["@react-router/dev@7.9.1", "", { "dependencies": { "@babel/core": "^7.27.7", "@babel/generator": "^7.27.5", "@babel/parser": "^7.27.7", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/preset-typescript": "^7.27.1", "@babel/traverse": "^7.27.7", "@babel/types": "^7.27.7", "@npmcli/package-json": "^4.0.1", "@react-router/node": "7.9.1", "arg": "^5.0.1", "babel-dead-code-elimination": "^1.0.6", "chokidar": "^4.0.0", "dedent": "^1.5.3", "es-module-lexer": "^1.3.1", "exit-hook": "2.2.1", "isbot": "^5.1.11", "jsesc": "3.0.2", "lodash": "^4.17.21", "pathe": "^1.1.2", "picocolors": "^1.1.1", "prettier": "^3.6.2", "react-refresh": "^0.14.0", "semver": "^7.3.7", "set-cookie-parser": "^2.6.0", "tinyglobby": "^0.2.14", "valibot": "^0.41.0", "vite-node": "^3.2.2" }, "peerDependencies": { "@react-router/serve": "^7.9.1", "@vitejs/plugin-rsc": "*", "react-router": "^7.9.1", "typescript": "^5.1.0", "vite": "^5.1.0 || ^6.0.0 || ^7.0.0", "wrangler": "^3.28.2 || ^4.0.0" }, "optionalPeers": ["@react-router/serve", "@vitejs/plugin-rsc", "typescript", "wrangler"], "bin": { "react-router": "bin.js" } }, "sha512-fW/qubsdHq1nsufHPLpXa6hiNvXXV9JBtWqRlJ02OOhFeaWERZw4rGoHjG1DCg8/QTTadgbzplmP97ZnzWPkcA=="], @@ -1190,6 +1203,8 @@ "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], + "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], "array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="], @@ -1270,7 +1285,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.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="], + "bun-types": ["bun-types@1.3.2", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-i/Gln4tbzKNuxP70OWhJRZz1MRfvqExowP7U6JKoI8cntFrtxg7RJK3jvz7wQW54UuvNC8tbKHHri5fy74FVqg=="], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], @@ -1480,6 +1495,8 @@ "detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="], + "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + "deterministic-object-hash": ["deterministic-object-hash@2.0.2", "", { "dependencies": { "base-64": "^1.0.0" } }, "sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ=="], "devalue": ["devalue@5.3.2", "", {}, "sha512-UDsjUbpQn9kvm68slnrs+mfxwFkIflOhkanmyabZ8zOYk8SMEIbJ3TK+88g70hSIeytu4y18f0z/hYHMTrXIWw=="], @@ -1700,6 +1717,8 @@ "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], + "get-port": ["get-port@5.1.1", "", {}, "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ=="], "get-port-please": ["get-port-please@3.2.0", "", {}, "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A=="], @@ -2384,8 +2403,14 @@ "react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="], + "react-remove-scroll": ["react-remove-scroll@2.7.1", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA=="], + + "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], + "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-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], + "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=="], @@ -2790,8 +2815,12 @@ "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], + "use-debounce": ["use-debounce@10.0.6", "", { "peerDependencies": { "react": "*" } }, "sha512-C5OtPyhAZgVoteO9heXMTdW7v/IbFI+8bSVKYCJrSmiWWCLsbUxiBSp4t9v0hNBTGY97bT72ydDIDyGSFWfwXg=="], + "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], + "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=="], @@ -3012,6 +3041,8 @@ "@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.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="], + "@types/d3-scale/@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="], "@types/xml2js/@types/node": ["@types/node@24.6.2", "", { "dependencies": { "undici-types": "~7.13.0" } }, "sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang=="], @@ -3090,7 +3121,7 @@ "dot-prop/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], - "elysia-api/zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="], + "elysia-api/zod": ["zod@4.1.12", "", {}, "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ=="], "eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -3360,6 +3391,8 @@ "@tanstack/server-functions-plugin/@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + "@types/bun/bun-types/@types/node": ["@types/node@24.6.2", "", { "dependencies": { "undici-types": "~7.13.0" } }, "sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang=="], + "@types/xml2js/@types/node/undici-types": ["undici-types@7.13.0", "", {}, "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], @@ -3474,6 +3507,8 @@ "@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="], + "@types/bun/bun-types/@types/node/undici-types": ["undici-types@7.13.0", "", {}, "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ=="], + "@unocss/cli/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "@unocss/vite/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], diff --git a/packages/elysia/lib/schema.ts b/packages/elysia/lib/schema.ts new file mode 100644 index 0000000..431f13b --- /dev/null +++ b/packages/elysia/lib/schema.ts @@ -0,0 +1,31 @@ +import { z } from "zod"; + +export const BiliVideoSchema = z.object({ + duration: z.number().nullable(), + id: z.number(), + aid: z.number(), + publishedAt: z.string().nullable(), + createdAt: z.string().nullable(), + description: z.string().nullable(), + bvid: z.string().nullable(), + uid: z.number().nullable(), + tags: z.string().nullable(), + title: z.string().nullable(), + status: z.number(), + coverUrl: z.string().nullable() +}); + +export const SongSchema = z.object({ + duration: z.number().nullable(), + name: z.string().nullable(), + id: z.number(), + aid: z.number().nullable(), + publishedAt: z.string().nullable(), + type: z.number().nullable(), + neteaseId: z.number().nullable(), + createdAt: z.string(), + updatedAt: z.string(), + deleted: z.boolean(), + image: z.string().nullable(), + producer: z.string().nullable() +}); diff --git a/packages/elysia/package.json b/packages/elysia/package.json index eaaee1b..7e53a85 100644 --- a/packages/elysia/package.json +++ b/packages/elysia/package.json @@ -15,7 +15,7 @@ "chalk": "^5.6.2", "elysia": "^1.4.0", "elysia-ip": "^1.0.10", - "zod": "^4.1.11" + "zod": "^4.1.12" }, "devDependencies": { "bun-types": "latest", diff --git a/packages/elysia/routes/search/index.ts b/packages/elysia/routes/search/index.ts index 0ff6e02..4cea4c3 100644 --- a/packages/elysia/routes/search/index.ts +++ b/packages/elysia/routes/search/index.ts @@ -1,8 +1,10 @@ -import { Elysia, t } from "elysia"; +import { Elysia } from "elysia"; import { db } from "@core/drizzle"; import { bilibiliMetadata, latestVideoSnapshot, songs } from "@core/drizzle/main/schema"; import { eq, like, or } from "drizzle-orm"; import type { BilibiliMetadataType, ProducerType, SongType } from "@core/drizzle/outerSchema"; +import { BiliVideoSchema, SongSchema } from "@elysia/lib/schema"; +import { z } from "zod"; interface SongSearchResult { type: "song"; @@ -27,7 +29,7 @@ const getSongSearchResult = async (searchQuery: string) => { .select() .from(songs) .innerJoin(latestVideoSnapshot, eq(songs.aid, latestVideoSnapshot.aid)) - .where(like(songs.name, `%${searchQuery}%`)) + .where(like(songs.name, `%${searchQuery}%`)); const results = data .map((song) => { @@ -88,7 +90,7 @@ const getVideoSearchResult = async (searchQuery: string) => { eq(bilibiliMetadata.bvid, searchQuery), eq(bilibiliMetadata.aid, extractAVID(searchQuery) || 0) ) - ) + ); return results.map((video) => ({ type: "bili-video" as "bili-video", data: { views: video.latest_video_snapshot.views, ...video.bilibili_metadata }, @@ -96,35 +98,8 @@ const getVideoSearchResult = async (searchQuery: string) => { })); }; -const BiliVideoDataSchema = t.Object({ - duration: t.Union([t.Number(), t.Null()]), - id: t.Number(), - aid: t.Number(), - publishedAt: t.Union([t.String(), t.Null()]), - createdAt: t.Union([t.String(), t.Null()]), - description: t.Union([t.String(), t.Null()]), - bvid: t.Union([t.String(), t.Null()]), - uid: t.Union([t.Number(), t.Null()]), - tags: t.Union([t.String(), t.Null()]), - title: t.Union([t.String(), t.Null()]), - status: t.Number(), - coverUrl: t.Union([t.String(), t.Null()]), - views: t.Number() -}); - -const SongDataSchema = t.Object({ - duration: t.Union([t.Number(), t.Null()]), - name: t.Union([t.String(), t.Null()]), - id: t.Number(), - aid: t.Union([t.Number(), t.Null()]), - publishedAt: t.Union([t.String(), t.Null()]), - type: t.Union([t.Number(), t.Null()]), - neteaseId: t.Union([t.Number(), t.Null()]), - createdAt: t.String(), - updatedAt: t.String(), - deleted: t.Boolean(), - image: t.Union([t.String(), t.Null()]), - producer: t.Union([t.String(), t.Null()]) +const BiliVideoDataSchema = BiliVideoSchema.extend({ + views: z.number() }); export const searchHandler = new Elysia({ prefix: "/search" }).get( @@ -136,34 +111,31 @@ export const searchHandler = new Elysia({ prefix: "/search" }).get( getVideoSearchResult(searchQuery) ]); - const combinedResults = [ - ...songResults, - ...videoResults - ]; + const combinedResults = [...songResults, ...videoResults]; return combinedResults.sort((a, b) => b.rank - a.rank); }, { response: { - 200: t.Array( - t.Union([ - t.Object({ - type: t.Literal("song"), - data: SongDataSchema, - rank: t.Number() + 200: z.array( + z.union([ + z.object({ + type: z.literal("song"), + data: SongSchema, + rank: z.number() }), - t.Object({ - type: t.Literal("bili-video"), + z.object({ + type: z.literal("bili-video"), data: BiliVideoDataSchema, - rank: t.Number() + rank: z.number() }) ]) ), - 404: t.Object({ - message: t.String() + 404: z.object({ + message: z.string() }) }, - query: t.Object({ - query: t.String() + query: z.object({ + query: z.string() }) } ); diff --git a/packages/elysia/routes/song/info.ts b/packages/elysia/routes/song/info.ts index e812a0f..ecf9ee5 100644 --- a/packages/elysia/routes/song/info.ts +++ b/packages/elysia/routes/song/info.ts @@ -66,12 +66,14 @@ export const songInfoHandler = new Elysia({ prefix: "/song" }) const songID = await getSongID(id); if (!songID) { return status(404, { + code: "SONG_NOT_FOUND", message: "Given song cannot be found." }); } const info = await getSongInfo(songID); if (!info) { return status(404, { + code: "SONG_NOT_FOUND", message: "Given song cannot be found." }); } @@ -117,12 +119,14 @@ export const songInfoHandler = new Elysia({ prefix: "/song" }) const songID = await getSongID(id); if (!songID) { return status(404, { + code: "SONG_NOT_FOUND", message: "Given song cannot be found." }); } const info = await getSongInfo(songID); if (!info) { return status(404, { + code: "SONG_NOT_FOUND", message: "Given song cannot be found." }); } @@ -152,7 +156,8 @@ export const songInfoHandler = new Elysia({ prefix: "/song" }) updated: t.Any() }), 404: t.Object({ - message: t.String() + message: t.String(), + code: t.String() }) }, body: t.Object({ diff --git a/packages/elysia/routes/video/snapshots.ts b/packages/elysia/routes/video/snapshots.ts new file mode 100644 index 0000000..3b5aa68 --- /dev/null +++ b/packages/elysia/routes/video/snapshots.ts @@ -0,0 +1,56 @@ +import { Elysia } from "elysia"; +import { dbMain } from "@core/drizzle"; +import { videoSnapshot } from "@core/drizzle/main/schema"; +import { bv2av } from "@elysia/lib/av_bv"; +import { ErrorResponseSchema } from "@elysia/src/schema"; +import { eq, desc } from "drizzle-orm"; +import z from "zod"; + +export const getVideoSnapshotsHandler = new Elysia({ prefix: "/video" }).get( + "/:id/snapshots", + async (c) => { + const id = c.params.id; + let aid: number | null = null; + + if (id.startsWith("BV1")) { + aid = bv2av(id as `BV1${string}`); + } else if (id.startsWith("av")) { + aid = Number.parseInt(id.slice(2)); + } else { + return c.status(400, { + code: "MALFORMED_SLOT", + message: + "We cannot parse the video ID, or we currently do not support this format.", + errors: [] + }); + } + + const data = await dbMain + .select() + .from(videoSnapshot) + .where(eq(videoSnapshot.aid, aid)) + .orderBy(desc(videoSnapshot.createdAt)); + + return data; + }, + { + response: { + 200: z.array( + z.object({ + id: z.number(), + createdAt: z.string(), + views: z.number(), + coins: z.number().nullable(), + likes: z.number().nullable(), + favorites: z.number().nullable(), + shares: z.number().nullable(), + danmakus: z.number().nullable(), + aid: z.number(), + replies: z.number().nullable() + }) + ), + 400: ErrorResponseSchema, + 500: ErrorResponseSchema + } + } +); diff --git a/packages/elysia/src/index.ts b/packages/elysia/src/index.ts index bbf801d..b8cdf4c 100644 --- a/packages/elysia/src/index.ts +++ b/packages/elysia/src/index.ts @@ -10,6 +10,7 @@ import { closeMileStoneHandler } from "@elysia/routes/song/milestone"; import { authHandler } from "@elysia/routes/auth"; import { onAfterHandler } from "./onAfterHandle"; import { searchHandler } from "@elysia/routes/search"; +import { getVideoSnapshotsHandler } from "@elysia/routes/video/snapshots"; const [host, port] = getBindingInfo(); logStartup(host, port); @@ -29,6 +30,7 @@ const app = new Elysia({ .use(songInfoHandler) .use(closeMileStoneHandler) .use(searchHandler) + .use(getVideoSnapshotsHandler) .listen(15412); export const VERSION = "0.7.0"; diff --git a/packages/temp_frontend/app/components/ui/dialog.tsx b/packages/temp_frontend/app/components/ui/dialog.tsx new file mode 100644 index 0000000..4e735b6 --- /dev/null +++ b/packages/temp_frontend/app/components/ui/dialog.tsx @@ -0,0 +1,141 @@ +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { XIcon } from "lucide-react" + +import { cn } from "@/lib//utils" + +function Dialog({ + ...props +}: React.ComponentProps) { + return +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogContent({ + className, + children, + showCloseButton = true, + ...props +}: React.ComponentProps & { + showCloseButton?: boolean +}) { + return ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} diff --git a/packages/temp_frontend/app/routes/song/[id]/info.tsx b/packages/temp_frontend/app/routes/song/[id]/info.tsx index acb94fe..b1bd164 100644 --- a/packages/temp_frontend/app/routes/song/[id]/info.tsx +++ b/packages/temp_frontend/app/routes/song/[id]/info.tsx @@ -8,32 +8,106 @@ import { Title } from "@/components/Title"; import { Search } from "@/components/Search"; import { Error } from "@/components/Error"; import { Layout } from "@/components/Layout"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; const app = treaty(import.meta.env.VITE_API_URL!); type SongInfo = Awaited["info"]["get"]>>["data"]; +type Snapshots = Awaited["snapshots"]["get"]>>["data"]; type SongInfoError = Awaited["info"]["get"]>>["error"]; +type SnapshotsError = Awaited["snapshots"]["get"]>>["error"]; export async function clientLoader({ params }: Route.LoaderArgs) { return { id: params.id }; } +const SnapshotsView = ({ snapshots }: { snapshots: Snapshots | null }) => { + if (!snapshots) { + return ( + <> +

历史快照

+ + + ); + } + return ( +
+

历史快照

+ + + + + + + + + + + + + {snapshots.map((snapshot: Exclude[number]) => ( + + + + + + + + + ))} + +
日期播放量弹幕数点赞数收藏数硬币数
{new Date(snapshot.createdAt).toLocaleDateString()}{snapshot.views}{snapshot.danmakus}{snapshot.likes}{snapshot.favorites}{snapshot.coins}
+
+ ); +}; + export default function SongInfo({ loaderData }: Route.ComponentProps) { const [data, setData] = useState(null); - const [error, setError] = useState(null); + const [snapshots, setSnapshots] = useState(null); + const [error, setError] = useState(null); + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [songName, setSongName] = useState(""); + + const getSnapshots = async (aid: number) => { + const { data, error } = await app.video({ id: `av${aid}` }).snapshots.get(); + if (error) { + console.log(error); + setError(error); + return; + } + setSnapshots(data); + }; + + const getInfo = async () => { + const { data, error } = await app.song({ id: loaderData.id }).info.get(); + if (error) { + console.log(error); + setError(error); + return; + } + setData(data); + }; useEffect(() => { - (async () => { - const { data, error } = await app.song({ id: loaderData.id }).info.get(); - if (error) { - console.log(error); - setError(error); - return; - } - setData(data); - })(); + getInfo(); }, []); + useEffect(() => { + if (!data) return; + const aid = data.aid; + if (!aid) return; + getSnapshots(aid); + }, [data]); + + // Update local song name when data changes + useEffect(() => { + if (data?.name) { + setSongName(data.name); + } + }, [data?.name]); + if (!data && !error) { return ( @@ -56,7 +130,9 @@ export default function SongInfo({ loaderData }: Route.ComponentProps) {

无法找到曲目

- 点此收录 + + 点此收录 +
); @@ -72,9 +148,13 @@ export default function SongInfo({ loaderData }: Route.ComponentProps) { return `${minutes}:${seconds}`; }; - const songNameOnChange = async (e: React.FocusEvent) => { - const name = e.target.textContent; - await app.song({ id: loaderData.id }).info.patch({ name: name || undefined }); + const handleSongNameChange = async () => { + if (songName.trim() === "") return; + + await app.song({ id: loaderData.id }).info.patch({ name: songName }); + setIsDialogOpen(false); + // Refresh the data to show the updated name + getInfo(); }; return ( @@ -93,9 +173,32 @@ export default function SongInfo({ loaderData }: Route.ComponentProps) { /> )}
-

- {data!.name ? data!.name : "未知歌曲名"} -

+
+

setIsDialogOpen(true)}> + {data!.name ? data!.name : "未知歌曲名"} +

+ + + + 编辑歌曲名称 + +
+ setSongName(e.target.value)} + placeholder="请输入歌曲名称" + className="w-full" + /> +
+ + +
+
+
+
+
{data!.duration ? formatDuration(data!.duration) : "未知时长"} @@ -105,6 +208,7 @@ export default function SongInfo({ loaderData }: Route.ComponentProps) {
+ ); diff --git a/packages/temp_frontend/package.json b/packages/temp_frontend/package.json index 630defc..3c1f361 100644 --- a/packages/temp_frontend/package.json +++ b/packages/temp_frontend/package.json @@ -13,6 +13,7 @@ "@elysiajs/eden": "^1.4.1", "@nivo/core": "^0.99.0", "@nivo/line": "^0.99.0", + "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tabs": "^1.1.13", "@react-router/node": "^7.7.1",