update: display logic of milestone videos in homepage
This commit is contained in:
parent
2bbe2ef51d
commit
e6623b3937
86
bun.lock
86
bun.lock
@ -1,6 +1,5 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "cvsa",
|
||||
@ -126,6 +125,7 @@
|
||||
"@nivo/line": "^0.99.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
"@radix-ui/react-progress": "^1.1.8",
|
||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
@ -789,13 +789,13 @@
|
||||
|
||||
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
|
||||
|
||||
"@react-router/dev": ["@react-router/dev@7.9.5", "", { "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.5", "@remix-run/node-fetch-server": "^0.9.0", "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", "p-map": "^7.0.3", "pathe": "^1.1.2", "picocolors": "^1.1.1", "prettier": "^3.6.2", "react-refresh": "^0.14.0", "semver": "^7.3.7", "tinyglobby": "^0.2.14", "valibot": "^1.1.0", "vite-node": "^3.2.2" }, "peerDependencies": { "@react-router/serve": "^7.9.5", "@vitejs/plugin-rsc": "*", "react-router": "^7.9.5", "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-MkWI4zN7VbQ0tteuJtX5hmDINNS26IW236a8lM8+o1344xdnT/ZsBvcUh8AkzDdCRYEz1blgzgirpj0Wc1gmXg=="],
|
||||
"@react-router/dev": ["@react-router/dev@7.9.6", "", { "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.6", "@remix-run/node-fetch-server": "^0.9.0", "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", "p-map": "^7.0.3", "pathe": "^1.1.2", "picocolors": "^1.1.1", "prettier": "^3.6.2", "react-refresh": "^0.14.0", "semver": "^7.3.7", "tinyglobby": "^0.2.14", "valibot": "^1.1.0", "vite-node": "^3.2.2" }, "peerDependencies": { "@react-router/serve": "^7.9.6", "@vitejs/plugin-rsc": "*", "react-router": "^7.9.6", "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-pBkbczGwI+NcZPcK8JPvWGWdjUpT/+okXYp6IXvt7zI3WLxr5hQLLRox5FkLiVxkykbqARO1hk9NRp9KFwJ2sA=="],
|
||||
|
||||
"@react-router/express": ["@react-router/express@7.9.5", "", { "dependencies": { "@react-router/node": "7.9.5" }, "peerDependencies": { "express": "^4.17.1 || ^5", "react-router": "7.9.5", "typescript": "^5.1.0" }, "optionalPeers": ["typescript"] }, "sha512-Mg94Tw9JSaRuwkvIC6PaODRzsLs6mo70ppz5qdIK/G3iotSxsH08TDNdzot7CaXXevk/pIiD/+Tbn0H/asHsYA=="],
|
||||
"@react-router/express": ["@react-router/express@7.9.6", "", { "dependencies": { "@react-router/node": "7.9.6" }, "peerDependencies": { "express": "^4.17.1 || ^5", "react-router": "7.9.6", "typescript": "^5.1.0" }, "optionalPeers": ["typescript"] }, "sha512-YykIWqZSkcaOnC72k0BtPZJK9781Ge623pWkTn0svzFLsqWW2/tX/Y1/Le6eG2xWrGeGfaeTSzi9dy3agP0OIw=="],
|
||||
|
||||
"@react-router/node": ["@react-router/node@7.9.5", "", { "dependencies": { "@mjackson/node-fetch-server": "^0.2.0" }, "peerDependencies": { "react-router": "7.9.5", "typescript": "^5.1.0" }, "optionalPeers": ["typescript"] }, "sha512-3mDd32mXh3gEkG0cLPnUaoLkY1pApsTPqn7O1j+P8aLf997uYz5lYDjt33vtMhaotlRM0x+5JziAKtz/76YBpQ=="],
|
||||
"@react-router/node": ["@react-router/node@7.9.6", "", { "dependencies": { "@mjackson/node-fetch-server": "^0.2.0" }, "peerDependencies": { "react-router": "7.9.6", "typescript": "^5.1.0" }, "optionalPeers": ["typescript"] }, "sha512-XzU8gPHwSl2Qh8/bOV30npbpH2fWOO3sFg+SwhX3+IddD1a/0C2KQzRiW/qAngkvZTJVdbca5Qp+FJjCCE7sNw=="],
|
||||
|
||||
"@react-router/serve": ["@react-router/serve@7.9.5", "", { "dependencies": { "@mjackson/node-fetch-server": "^0.2.0", "@react-router/express": "7.9.5", "@react-router/node": "7.9.5", "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.5" }, "bin": { "react-router-serve": "bin.js" } }, "sha512-sww8oDNqz8SgaXEQ3maqTuMlibCMpmWvLE0s5zyEyOQb1G99clYMcXceQ2HNU2jtXJkp+P5XI1CngpGpngyTnw=="],
|
||||
"@react-router/serve": ["@react-router/serve@7.9.6", "", { "dependencies": { "@mjackson/node-fetch-server": "^0.2.0", "@react-router/express": "7.9.6", "@react-router/node": "7.9.6", "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.6" }, "bin": { "react-router-serve": "bin.js" } }, "sha512-qIT8hp1RJ0VAHyXpfuwoO31b9evrjPLRhUugqYJ7BZLpyAwhRsJIaQvvj60yZwWBMF2/3LdZu7M39rf0FhL6Iw=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
@ -1883,7 +1883,7 @@
|
||||
|
||||
"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.5", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-JmxqrnBZ6E9hWmf02jzNn9Jm3UqyeimyiwzD69NjxGySG6lIz/1LVPsoTCwN7NBX2XjCEa1LIX5EMz1j2b6u6A=="],
|
||||
"react-router": ["react-router@7.9.6", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-Y1tUp8clYRXpfPITyuifmSoE2vncSME18uVLgaqyxh9H35JWpIfzHo+9y3Fzh5odk/jxPW29IgLgzcdwxGqyNA=="],
|
||||
|
||||
"react-smooth": ["react-smooth@4.0.4", "", { "dependencies": { "fast-equals": "^5.0.1", "prop-types": "^15.8.1", "react-transition-group": "^4.4.5" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q=="],
|
||||
|
||||
@ -2305,8 +2305,6 @@
|
||||
|
||||
"@react-router/dev/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
|
||||
|
||||
"@react-router/dev/prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
|
||||
|
||||
"@react-router/dev/react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="],
|
||||
|
||||
"@react-router/serve/express": ["express@4.21.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA=="],
|
||||
@ -2421,16 +2419,14 @@
|
||||
|
||||
"temp_frontend/@types/node": ["@types/node@20.19.24", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-FE5u0ezmi6y9OZEzlJfg37mqqf6ZDSF2V/NLjUyGrR9uTZ7Sb9F7bLNZ03S4XVUNRWGA7Ck4c1kK+YnuWjl+DA=="],
|
||||
|
||||
"temp_frontend/@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="],
|
||||
|
||||
"temp_frontend/@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||
|
||||
"temp_frontend/lucide-react": ["lucide-react@0.553.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-BRgX5zrWmNy/lkVAe0dXBgd7XQdZ3HTf+Hwe3c9WK6dqgnj9h+hxV+MDncM88xDWlCq27+TKvHGE70ViODNILw=="],
|
||||
|
||||
"temp_frontend/vite": ["vite@6.4.1", "", { "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-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="],
|
||||
|
||||
"tracker/@react-router/dev": ["@react-router/dev@7.9.6", "", { "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.6", "@remix-run/node-fetch-server": "^0.9.0", "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", "p-map": "^7.0.3", "pathe": "^1.1.2", "picocolors": "^1.1.1", "prettier": "^3.6.2", "react-refresh": "^0.14.0", "semver": "^7.3.7", "tinyglobby": "^0.2.14", "valibot": "^1.1.0", "vite-node": "^3.2.2" }, "peerDependencies": { "@react-router/serve": "^7.9.6", "@vitejs/plugin-rsc": "*", "react-router": "^7.9.6", "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-pBkbczGwI+NcZPcK8JPvWGWdjUpT/+okXYp6IXvt7zI3WLxr5hQLLRox5FkLiVxkykbqARO1hk9NRp9KFwJ2sA=="],
|
||||
|
||||
"tracker/@react-router/node": ["@react-router/node@7.9.6", "", { "dependencies": { "@mjackson/node-fetch-server": "^0.2.0" }, "peerDependencies": { "react-router": "7.9.6", "typescript": "^5.1.0" }, "optionalPeers": ["typescript"] }, "sha512-XzU8gPHwSl2Qh8/bOV30npbpH2fWOO3sFg+SwhX3+IddD1a/0C2KQzRiW/qAngkvZTJVdbca5Qp+FJjCCE7sNw=="],
|
||||
|
||||
"tracker/@react-router/serve": ["@react-router/serve@7.9.6", "", { "dependencies": { "@mjackson/node-fetch-server": "^0.2.0", "@react-router/express": "7.9.6", "@react-router/node": "7.9.6", "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.6" }, "bin": { "react-router-serve": "bin.js" } }, "sha512-qIT8hp1RJ0VAHyXpfuwoO31b9evrjPLRhUugqYJ7BZLpyAwhRsJIaQvvj60yZwWBMF2/3LdZu7M39rf0FhL6Iw=="],
|
||||
|
||||
"tracker/@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="],
|
||||
|
||||
"tracker/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="],
|
||||
@ -2443,8 +2439,6 @@
|
||||
|
||||
"tracker/lucide-react": ["lucide-react@0.555.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-D8FvHUGbxWBRQM90NZeIyhAvkFfsh3u9ekrMvJ30Z6gnpBHS6HC6ldLg7tL45hwiIz/u66eKDtdA23gwwGsAHA=="],
|
||||
|
||||
"tracker/react-router": ["react-router@7.9.6", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-Y1tUp8clYRXpfPITyuifmSoE2vncSME18uVLgaqyxh9H35JWpIfzHo+9y3Fzh5odk/jxPW29IgLgzcdwxGqyNA=="],
|
||||
|
||||
"tracker/vite": ["vite@7.2.4", "", { "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-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w=="],
|
||||
|
||||
"unconfig/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||
@ -2637,13 +2631,7 @@
|
||||
|
||||
"temp_frontend/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"tracker/@react-router/dev/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
|
||||
|
||||
"tracker/@react-router/dev/react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="],
|
||||
|
||||
"tracker/@react-router/serve/@react-router/express": ["@react-router/express@7.9.6", "", { "dependencies": { "@react-router/node": "7.9.6" }, "peerDependencies": { "express": "^4.17.1 || ^5", "react-router": "7.9.6", "typescript": "^5.1.0" }, "optionalPeers": ["typescript"] }, "sha512-YykIWqZSkcaOnC72k0BtPZJK9781Ge623pWkTn0svzFLsqWW2/tX/Y1/Le6eG2xWrGeGfaeTSzi9dy3agP0OIw=="],
|
||||
|
||||
"tracker/@react-router/serve/express": ["express@4.21.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA=="],
|
||||
"temp_frontend/@types/react/csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||
|
||||
"tracker/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
@ -2731,60 +2719,8 @@
|
||||
|
||||
"sucrase/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
"tracker/@react-router/serve/express/accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
|
||||
|
||||
"tracker/@react-router/serve/express/body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="],
|
||||
|
||||
"tracker/@react-router/serve/express/content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="],
|
||||
|
||||
"tracker/@react-router/serve/express/cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="],
|
||||
|
||||
"tracker/@react-router/serve/express/cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="],
|
||||
|
||||
"tracker/@react-router/serve/express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"tracker/@react-router/serve/express/finalhandler": ["finalhandler@1.3.1", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", "statuses": "2.0.1", "unpipe": "~1.0.0" } }, "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ=="],
|
||||
|
||||
"tracker/@react-router/serve/express/fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
|
||||
|
||||
"tracker/@react-router/serve/express/merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="],
|
||||
|
||||
"tracker/@react-router/serve/express/path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="],
|
||||
|
||||
"tracker/@react-router/serve/express/qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="],
|
||||
|
||||
"tracker/@react-router/serve/express/send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="],
|
||||
|
||||
"tracker/@react-router/serve/express/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=="],
|
||||
|
||||
"tracker/@react-router/serve/express/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
|
||||
|
||||
"tracker/@react-router/serve/express/type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="],
|
||||
|
||||
"@react-router/serve/express/accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"@react-router/serve/express/type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"tracker/@react-router/serve/express/accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"tracker/@react-router/serve/express/accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
|
||||
|
||||
"tracker/@react-router/serve/express/body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
|
||||
|
||||
"tracker/@react-router/serve/express/body-parser/raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="],
|
||||
|
||||
"tracker/@react-router/serve/express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"tracker/@react-router/serve/express/send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="],
|
||||
|
||||
"tracker/@react-router/serve/express/send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
|
||||
|
||||
"tracker/@react-router/serve/express/type-is/media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="],
|
||||
|
||||
"tracker/@react-router/serve/express/type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"tracker/@react-router/serve/express/accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"tracker/@react-router/serve/express/type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,30 +8,27 @@ import { BiliVideoSchema } from "@backend/lib/schema";
|
||||
type MileStoneType = "dendou" | "densetsu" | "shinwa";
|
||||
|
||||
const range = {
|
||||
dendou: [0, 100000, 2160],
|
||||
densetsu: [100000, 1000000, 8760],
|
||||
shinwa: [1000000, 10000000, 43800]
|
||||
dendou: [0, 100000],
|
||||
densetsu: [100000, 1000000],
|
||||
shinwa: [1000000, 10000000]
|
||||
};
|
||||
|
||||
export const closeMileStoneHandler = new Elysia({ prefix: "/songs" }).use(serverTiming()).get(
|
||||
"/close-milestone/:type",
|
||||
async ({ params, timeLog }) => {
|
||||
timeLog.startTime("retrieveCandidates");
|
||||
async ({ params }) => {
|
||||
const type = params.type;
|
||||
const offset = params.offset;
|
||||
const limit = params.limit;
|
||||
const min = range[type as MileStoneType][0];
|
||||
const max = range[type as MileStoneType][1];
|
||||
return db
|
||||
const query = db
|
||||
.select()
|
||||
.from(eta)
|
||||
.innerJoin(bilibiliMetadata, eq(bilibiliMetadata.aid, eta.aid))
|
||||
.where(
|
||||
and(
|
||||
gte(eta.currentViews, min),
|
||||
lt(eta.currentViews, max),
|
||||
lt(eta.eta, range[type as MileStoneType][2])
|
||||
)
|
||||
)
|
||||
.orderBy(eta.eta);
|
||||
.where(and(gte(eta.currentViews, min), lt(eta.currentViews, max)))
|
||||
.orderBy(eta.eta)
|
||||
.$dynamic();
|
||||
return query.limit(limit || 20).offset(offset || 0);
|
||||
},
|
||||
{
|
||||
response: {
|
||||
@ -51,6 +48,11 @@ export const closeMileStoneHandler = new Elysia({ prefix: "/songs" }).use(server
|
||||
message: t.String()
|
||||
})
|
||||
},
|
||||
params: t.Object({
|
||||
type: t.String({ enum: ["dendou", "densetsu", "shinwa"] }),
|
||||
offset: t.Optional(t.Number()),
|
||||
limit: t.Optional(t.Number())
|
||||
}),
|
||||
detail: {
|
||||
summary: "Get songs close to milestones",
|
||||
description:
|
||||
|
||||
@ -5,19 +5,21 @@ export function Error({ error }: { error: { status: number; value: { message?: s
|
||||
return (
|
||||
<div className="w-screen min-h-screen flex items-center justify-center">
|
||||
<Title title="出错了" />
|
||||
<div className="max-w-md w-full mx-4 bg-gray-100 dark:bg-neutral-900 rounded-2xl
|
||||
shadow-lg p-6 flex flex-col gap-4 items-center text-center">
|
||||
<div
|
||||
className="max-w-md w-full mx-4 bg-gray-100 dark:bg-neutral-900 rounded-2xl
|
||||
shadow-lg p-6 flex flex-col gap-4 items-center text-center"
|
||||
>
|
||||
<div className="w-16 h-16 flex items-center justify-center rounded-full bg-red-500 text-white text-3xl">
|
||||
<TriangleAlert size={34} className="-translate-y-0.5" />
|
||||
</div>
|
||||
<h1 className="text-3xl font-semibold text-neutral-900 dark:text-neutral-100">出错了</h1>
|
||||
<p className="text-neutral-700 dark:text-neutral-300">状态码:{error.status}</p>
|
||||
{error.value.message && (
|
||||
<p className="text-neutral-600 dark:text-neutral-400 break-words">
|
||||
<span className="font-medium text-neutral-700 dark:text-neutral-300">错误信息</span>
|
||||
<br />
|
||||
{error.value.message}
|
||||
</p>
|
||||
<p className="text-neutral-600 dark:text-neutral-400 wrap-break-word">{error.value.message}</p>
|
||||
)}
|
||||
{error.status === 404 && (
|
||||
<a href="/" className="hover:underline">
|
||||
返回首页
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
22
packages/temp_frontend/app/components/ui/label.tsx
Normal file
22
packages/temp_frontend/app/components/ui/label.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
|
||||
import { cn } from "@/lib//utils"
|
||||
|
||||
function Label({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
return (
|
||||
<LabelPrimitive.Root
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Label }
|
||||
@ -1,56 +1,44 @@
|
||||
import * as React from "react"
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
||||
import * as React from "react";
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
||||
|
||||
import { cn } from "@/lib//utils"
|
||||
import { cn } from "@/lib//utils";
|
||||
|
||||
function ScrollArea({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.Root
|
||||
data-slot="scroll-area"
|
||||
className={cn("relative", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
data-slot="scroll-area-viewport"
|
||||
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
|
||||
>
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
)
|
||||
}
|
||||
const ScrollArea = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||
>(({ className, children, onScroll, ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root className={cn("relative overflow-hidden", className)} {...props}>
|
||||
<ScrollAreaPrimitive.Viewport ref={ref} className="h-full w-full rounded-[inherit]" onScroll={onScroll}>
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
));
|
||||
|
||||
function ScrollBar({
|
||||
className,
|
||||
orientation = "vertical",
|
||||
...props
|
||||
className,
|
||||
orientation = "vertical",
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
data-slot="scroll-area-scrollbar"
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none p-px transition-colors select-none",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||
data-slot="scroll-area-thumb"
|
||||
className="bg-border relative flex-1 rounded-full"
|
||||
/>
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
)
|
||||
return (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
data-slot="scroll-area-scrollbar"
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none p-px transition-colors select-none",
|
||||
orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent",
|
||||
orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||
data-slot="scroll-area-thumb"
|
||||
className="bg-border relative flex-1 rounded-full"
|
||||
/>
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
);
|
||||
}
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
||||
export { ScrollArea, ScrollBar };
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { type RouteConfig, index, route } from "@react-router/dev/routes";
|
||||
|
||||
export default [
|
||||
index("routes/home.tsx"),
|
||||
index("routes/home/index.tsx"),
|
||||
route("song/:id/info", "routes/song/[id]/info/index.tsx"),
|
||||
route("song/:id/add", "routes/song/[id]/add.tsx"),
|
||||
route("search", "routes/search/index.tsx"),
|
||||
|
||||
@ -1,222 +0,0 @@
|
||||
import { Layout } from "@/components/Layout";
|
||||
import type { Route } from "./+types/home";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { treaty } from "@elysiajs/eden";
|
||||
import type { App } from "@backend/src";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { formatDateTime } from "@/components/SearchResults";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { addHoursToNow, formatHours } from "./song/[id]/info";
|
||||
|
||||
// @ts-ignore idk
|
||||
const app = treaty<App>(import.meta.env.VITE_API_URL!);
|
||||
|
||||
type CloseMilestoneInfo = Awaited<ReturnType<ReturnType<(typeof app.songs)["close-milestone"]>["get"]>>["data"];
|
||||
type CloseMilestoneError = Awaited<ReturnType<ReturnType<(typeof app.songs)["close-milestone"]>["get"]>>["error"];
|
||||
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [{ title: "中V档案馆" }];
|
||||
}
|
||||
|
||||
type MilestoneType = "dendou" | "densetsu" | "shinwa";
|
||||
|
||||
const milestoneConfig = {
|
||||
dendou: { name: "殿堂", range: [90000, 99999], target: 100000 },
|
||||
densetsu: { name: "传说", range: [900000, 999999], target: 1000000 },
|
||||
shinwa: { name: "神话", range: [5000000, 9999999], target: 10000000 },
|
||||
};
|
||||
|
||||
export default function Home() {
|
||||
const [input, setInput] = useState("");
|
||||
const [milestoneType, setMilestoneType] = useState<MilestoneType>("shinwa");
|
||||
const [closeMilestoneInfo, setCloseMilestoneInfo] = useState<CloseMilestoneInfo>();
|
||||
const [closeMilestoneError, setCloseMilestoneError] = useState<CloseMilestoneError>();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const fetchMilestoneData = async (type: MilestoneType) => {
|
||||
setIsLoading(true);
|
||||
setCloseMilestoneError(undefined);
|
||||
const { data, error } = await app.songs["close-milestone"]({ type }).get();
|
||||
if (error) {
|
||||
setCloseMilestoneError(error);
|
||||
} else {
|
||||
setCloseMilestoneInfo(data);
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchMilestoneData(milestoneType);
|
||||
}, [milestoneType]);
|
||||
|
||||
const MilestoneVideoCard = ({ video }: { video: NonNullable<CloseMilestoneInfo>[number] }) => {
|
||||
const config = milestoneConfig[milestoneType];
|
||||
const remainingViews = config.target - video.eta.currentViews;
|
||||
const progressPercentage = (video.eta.currentViews / config.target) * 100;
|
||||
|
||||
return (
|
||||
<Card className="px-3 max-md:py-3 md:px-4 my-4 gap-0">
|
||||
<div className="w-full flex items-start space-x-4 mb-4">
|
||||
{video.bilibili_metadata.coverUrl && (
|
||||
<img
|
||||
src={video.bilibili_metadata.coverUrl}
|
||||
alt="视频封面"
|
||||
className="h-25 w-40 rounded-sm object-cover shrink-0"
|
||||
referrerPolicy="no-referrer"
|
||||
loading="lazy"
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col w-full justify-between">
|
||||
<h3 className="text-sm sm:text-lg font-medium line-clamp-2 text-wrap mb-2">
|
||||
<a href={`/song/av${video.bilibili_metadata.aid}/info`} className="hover:underline">
|
||||
{video.bilibili_metadata.title}
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
<div className="space-y-2 text-xs text-muted-foreground">
|
||||
<div className="flex items-center justify-between">
|
||||
<span>当前播放: {video.eta.currentViews.toLocaleString()}</span>
|
||||
<span>目标: {config.target.toLocaleString()}</span>
|
||||
</div>
|
||||
|
||||
<Progress value={progressPercentage} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-5 text-xs text-muted-foreground mb-2">
|
||||
<div>
|
||||
<p>剩余播放: {remainingViews.toLocaleString()}</p>
|
||||
<p>预计达成: {formatHours(video.eta.eta)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p>播放速度: {Math.round(video.eta.speed)}/小时</p>
|
||||
<p>达成时间: {addHoursToNow(video.eta.eta)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 text-xs text-muted-foreground">
|
||||
{video.bilibili_metadata.publishedAt && (
|
||||
<span className="stat-num">
|
||||
发布于 {formatDateTime(new Date(video.bilibili_metadata.publishedAt))}
|
||||
</span>
|
||||
)}
|
||||
<a
|
||||
href={`https://www.bilibili.com/video/av${video.bilibili_metadata.aid}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-pink-400 text-xs hover:underline"
|
||||
>
|
||||
观看视频
|
||||
</a>
|
||||
<a
|
||||
href={`/song/av${video.bilibili_metadata.aid}/info`}
|
||||
className="text-xs text-secondary-foreground hover:underline"
|
||||
>
|
||||
查看详情
|
||||
</a>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
const MilestoneVideos = () => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="bg-white dark:bg-neutral-800 rounded-lg shadow-sm border border-gray-200 dark:border-neutral-700 p-4"
|
||||
>
|
||||
<div className="flex items-start space-x-4">
|
||||
<Skeleton className="h-21 w-36 rounded-sm" />
|
||||
<div className="flex-1 space-y-2">
|
||||
<Skeleton className="h-6 w-3/4" />
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-2/3" />
|
||||
<Skeleton className="h-4 w-1/2" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (closeMilestoneError) {
|
||||
return (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-red-500">加载失败: {closeMilestoneError.value?.message || "未知错误"}</p>
|
||||
<Button variant="outline" className="mt-4" onClick={() => fetchMilestoneData(milestoneType)}>
|
||||
重试
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!closeMilestoneInfo || closeMilestoneInfo.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-secondary-foreground">暂无接近{milestoneConfig[milestoneType].name}的视频</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
找到 {closeMilestoneInfo.length} 个接近{milestoneConfig[milestoneType].name}的视频
|
||||
</p>
|
||||
<ScrollArea className="h-140 w-full">
|
||||
{closeMilestoneInfo.map((video) => (
|
||||
<MilestoneVideoCard key={video.bilibili_metadata.aid} video={video} />
|
||||
))}
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<h2 className="text-2xl mt-5 mb-6">小工具</h2>
|
||||
<div className="flex max-sm:flex-col sm:items-center gap-7 mb-8">
|
||||
<Button>
|
||||
<a href="/util/time-calculator">时间计算器</a>
|
||||
</Button>
|
||||
|
||||
<div className="flex sm:w-96 gap-3">
|
||||
<Input placeholder="输入BV号或av号" value={input} onChange={(e) => setInput(e.target.value)} />
|
||||
<Button>
|
||||
<a href={`/song/${input}/add`}>收录视频</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 className="text-2xl mb-4">即将达成成就</h2>
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<Select value={milestoneType} onValueChange={(value: MilestoneType) => setMilestoneType(value)}>
|
||||
<SelectTrigger className="w-20">
|
||||
<SelectValue placeholder="成就" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="dendou">殿堂</SelectItem>
|
||||
<SelectItem value="densetsu">传说</SelectItem>
|
||||
<SelectItem value="shinwa">神话</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
播放量在 {milestoneConfig[milestoneType].range[0].toLocaleString()} -{" "}
|
||||
{milestoneConfig[milestoneType].range[1].toLocaleString()} 之间,即将达成
|
||||
{milestoneConfig[milestoneType].name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<MilestoneVideos />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
174
packages/temp_frontend/app/routes/home/Milestone.tsx
Normal file
174
packages/temp_frontend/app/routes/home/Milestone.tsx
Normal file
@ -0,0 +1,174 @@
|
||||
import React, { useState, useCallback, useEffect, useRef } from "react";
|
||||
import { treaty } from "@elysiajs/eden";
|
||||
import type { App } from "@backend/src";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { MilestoneVideoCard } from "./MilestoneVideoCard";
|
||||
|
||||
// @ts-ignore idk
|
||||
const app = treaty<App>(import.meta.env.VITE_API_URL!);
|
||||
|
||||
export type CloseMilestoneInfo = Awaited<ReturnType<ReturnType<(typeof app.songs)["close-milestone"]>["get"]>>["data"];
|
||||
type CloseMilestoneError = Awaited<ReturnType<ReturnType<(typeof app.songs)["close-milestone"]>["get"]>>["error"];
|
||||
|
||||
export type MilestoneType = "dendou" | "densetsu" | "shinwa";
|
||||
|
||||
export const milestoneConfig: Record<MilestoneType, { name: string; range: [number, number]; target: number }> = {
|
||||
dendou: { name: "殿堂", range: [90000, 99999], target: 100000 },
|
||||
densetsu: { name: "传说", range: [900000, 999999], target: 1000000 },
|
||||
shinwa: { name: "神话", range: [5000000, 9999999], target: 10000000 },
|
||||
};
|
||||
|
||||
export const MilestoneVideos: React.FC = () => {
|
||||
const [milestoneType, setMilestoneType] = useState<MilestoneType>("shinwa");
|
||||
const [milestoneData, setMilestoneData] = useState<CloseMilestoneInfo>([]);
|
||||
const [closeMilestoneError, setCloseMilestoneError] = useState<CloseMilestoneError>();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
||||
const [offset, setOffset] = useState(0);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
|
||||
const scrollContainer = useRef<HTMLDivElement>(null);
|
||||
|
||||
const fetchMilestoneData = useCallback(
|
||||
async (type: MilestoneType, reset: boolean = false) => {
|
||||
const currentOffset = reset ? 0 : offset;
|
||||
|
||||
if (!reset) {
|
||||
setIsLoadingMore(true);
|
||||
} else {
|
||||
setIsLoading(true);
|
||||
}
|
||||
|
||||
setCloseMilestoneError(undefined);
|
||||
|
||||
try {
|
||||
const { data, error } = await app.songs["close-milestone"]({ type }).get({
|
||||
query: {
|
||||
offset: currentOffset,
|
||||
limit: 20,
|
||||
},
|
||||
});
|
||||
|
||||
if (error) {
|
||||
setCloseMilestoneError(error);
|
||||
} else {
|
||||
if (reset) {
|
||||
setMilestoneData(data);
|
||||
} else {
|
||||
setMilestoneData((prev) => [...prev!, ...data]);
|
||||
}
|
||||
setHasMore(data.length >= 20);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Fetch error:", err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setIsLoadingMore(false);
|
||||
}
|
||||
},
|
||||
[offset],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setOffset(0);
|
||||
setHasMore(true);
|
||||
setMilestoneData([]);
|
||||
fetchMilestoneData(milestoneType, true);
|
||||
}, [milestoneType]);
|
||||
|
||||
useEffect(() => {
|
||||
if (offset > 0 && hasMore && !isLoadingMore) {
|
||||
fetchMilestoneData(milestoneType);
|
||||
}
|
||||
}, [offset]);
|
||||
|
||||
const handleScroll = useCallback(
|
||||
(e: React.UIEvent<HTMLDivElement>) => {
|
||||
const target = e.currentTarget;
|
||||
const { scrollHeight, scrollTop, clientHeight } = target;
|
||||
|
||||
if (scrollTop + clientHeight >= scrollHeight - 400 && !isLoadingMore && hasMore) {
|
||||
setOffset((prev) => prev + 20);
|
||||
}
|
||||
},
|
||||
[hasMore, isLoadingMore],
|
||||
);
|
||||
|
||||
const renderContent = () => {
|
||||
if (!milestoneData) return null;
|
||||
|
||||
if (isLoading && milestoneData.length === 0) {
|
||||
return (
|
||||
<ScrollArea className="h-140 w-full">
|
||||
<div className="h-[0.1px]"></div>
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="rounded-xl my-4 shadow-sm border border-gray-200 dark:border-neutral-700"
|
||||
>
|
||||
<Skeleton className="h-49 sm:h-55 rounded-xl" />
|
||||
</div>
|
||||
))}
|
||||
</ScrollArea>
|
||||
);
|
||||
}
|
||||
|
||||
if (closeMilestoneError && milestoneData.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-red-500">加载失败: {closeMilestoneError.value?.message || "未知错误"}</p>
|
||||
<Button variant="outline" className="mt-4" onClick={() => fetchMilestoneData(milestoneType, true)}>
|
||||
重试
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (milestoneData.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-secondary-foreground">暂无接近{milestoneConfig[milestoneType].name}的视频</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<ScrollArea className="h-140 w-full" ref={scrollContainer} onScroll={handleScroll}>
|
||||
{milestoneData.map((video) => (
|
||||
<MilestoneVideoCard
|
||||
key={video.bilibili_metadata.aid}
|
||||
video={video}
|
||||
milestoneType={milestoneType}
|
||||
/>
|
||||
))}
|
||||
{isLoadingMore && (
|
||||
<div className="rounded-xl my-4 px-3 py-4">
|
||||
<Skeleton className="h-40 rounded-xl" />
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-between mt-6 mb-2">
|
||||
<h2 className="text-2xl font-medium">成就助攻</h2>
|
||||
<Tabs value={milestoneType} onValueChange={(value: string) => setMilestoneType(value as MilestoneType)}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="dendou">殿堂</TabsTrigger>
|
||||
<TabsTrigger value="densetsu">传说</TabsTrigger>
|
||||
<TabsTrigger value="shinwa">神话</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
{renderContent()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,81 @@
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { formatDateTime } from "@/components/SearchResults";
|
||||
import { addHoursToNow, formatHours } from "../song/[id]/info";
|
||||
import { milestoneConfig, type CloseMilestoneInfo, type MilestoneType } from "./Milestone";
|
||||
|
||||
export const MilestoneVideoCard = ({
|
||||
video,
|
||||
milestoneType,
|
||||
}: {
|
||||
video: NonNullable<CloseMilestoneInfo>[number];
|
||||
milestoneType: MilestoneType;
|
||||
}) => {
|
||||
const config = milestoneConfig[milestoneType];
|
||||
const remainingViews = config.target - video.eta.currentViews;
|
||||
const progressPercentage = (video.eta.currentViews / config.target) * 100;
|
||||
|
||||
return (
|
||||
<Card className="px-3 max-md:py-3 md:px-4 my-4 gap-0">
|
||||
<div className="w-full flex items-start space-x-4 mb-4">
|
||||
{video.bilibili_metadata.coverUrl && (
|
||||
<img
|
||||
src={video.bilibili_metadata.coverUrl}
|
||||
alt="视频封面"
|
||||
className="h-25 w-40 rounded-sm object-cover shrink-0"
|
||||
referrerPolicy="no-referrer"
|
||||
loading="lazy"
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col w-full justify-between">
|
||||
<h3 className="text-sm sm:text-lg font-medium line-clamp-2 text-wrap mb-2">
|
||||
<a href={`/song/av${video.bilibili_metadata.aid}/info`} className="hover:underline">
|
||||
{video.bilibili_metadata.title}
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
<div className="space-y-2 text-xs text-muted-foreground">
|
||||
<div className="flex items-center justify-between">
|
||||
<span>当前播放: {video.eta.currentViews.toLocaleString()}</span>
|
||||
<span>目标: {config.target.toLocaleString()}</span>
|
||||
</div>
|
||||
|
||||
<Progress value={progressPercentage} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-5 text-xs text-muted-foreground mb-2">
|
||||
<div>
|
||||
<p>剩余播放: {remainingViews.toLocaleString()}</p>
|
||||
<p>预计达成: {formatHours(video.eta.eta)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p>播放速度: {Math.round(video.eta.speed)}/小时</p>
|
||||
<p>达成时间: {addHoursToNow(video.eta.eta)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 text-xs text-muted-foreground">
|
||||
{video.bilibili_metadata.publishedAt && (
|
||||
<span className="stat-num">
|
||||
发布于 {formatDateTime(new Date(video.bilibili_metadata.publishedAt))}
|
||||
</span>
|
||||
)}
|
||||
<a
|
||||
href={`https://www.bilibili.com/video/av${video.bilibili_metadata.aid}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-pink-400 text-xs hover:underline"
|
||||
>
|
||||
观看视频
|
||||
</a>
|
||||
<a
|
||||
href={`/song/av${video.bilibili_metadata.aid}/info`}
|
||||
className="text-xs text-secondary-foreground hover:underline"
|
||||
>
|
||||
查看详情
|
||||
</a>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
33
packages/temp_frontend/app/routes/home/index.tsx
Normal file
33
packages/temp_frontend/app/routes/home/index.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { Layout } from "@/components/Layout";
|
||||
import type { Route } from "./+types/index";
|
||||
import { useState } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { MilestoneVideos } from "@/routes/home/Milestone";
|
||||
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [{ title: "中V档案馆" }];
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const [input, setInput] = useState("");
|
||||
return (
|
||||
<Layout>
|
||||
<h2 className="text-2xl font-medium mt-8 mb-4">小工具</h2>
|
||||
<div className="flex max-sm:flex-col sm:items-center gap-7 mb-8">
|
||||
<a href="/time-calculator">
|
||||
<Button>时间计算器</Button>
|
||||
</a>
|
||||
|
||||
<div className="flex sm:w-96 gap-3">
|
||||
<Input placeholder="输入 BV 号或 av 号" value={input} onChange={(e) => setInput(e.target.value)} />
|
||||
<a href={`/song/${input}/add`}>
|
||||
<Button>收录视频</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MilestoneVideos />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
@ -2,23 +2,19 @@ import type { Route } from "./+types/add";
|
||||
import { treaty } from "@elysiajs/eden";
|
||||
import type { App } from "@backend/src";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { TriangleAlert, CheckCircle, Clock, AlertCircle } from "lucide-react";
|
||||
import { CheckCircle, Clock, AlertCircle } from "lucide-react";
|
||||
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";
|
||||
import { toast } from "sonner";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { biliIDToAID } from "@backend/lib/bilibiliID";
|
||||
import { Error } from "@/components/Error";
|
||||
|
||||
// @ts-ignore idk
|
||||
const app = treaty<App>(import.meta.env.VITE_API_URL!);
|
||||
|
||||
type SongInfo = Awaited<ReturnType<ReturnType<typeof app.song>["info"]["get"]>>["data"];
|
||||
type SongInfoError = Awaited<ReturnType<ReturnType<typeof app.song>["info"]["get"]>>["error"];
|
||||
type ImportStatus = {
|
||||
id: string;
|
||||
state: string;
|
||||
@ -34,6 +30,10 @@ export default function SongInfo({ loaderData }: Route.ComponentProps) {
|
||||
const [isImporting, setIsImporting] = useState(false);
|
||||
const [importStatus, setImportStatus] = useState<ImportStatus | null>(null);
|
||||
const [importInterval, setImportInterval] = useState<NodeJS.Timeout | null>(null);
|
||||
const aid = biliIDToAID(loaderData.id);
|
||||
if (!aid) {
|
||||
return <Error error={{ status: 404, value: { message: "找不到页面" } }} />;
|
||||
}
|
||||
|
||||
const importSong = async () => {
|
||||
const response = await app.song.import.bilibili.post(
|
||||
@ -51,7 +51,6 @@ export default function SongInfo({ loaderData }: Route.ComponentProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
// @ts-ignore - Type issues with Eden treaty
|
||||
const jobID = response.data?.jobID;
|
||||
if (!jobID) {
|
||||
toast.error("导入失败:未收到任务ID");
|
||||
@ -89,8 +88,8 @@ export default function SongInfo({ loaderData }: Route.ComponentProps) {
|
||||
// Redirect to song info page after successful import
|
||||
setTimeout(() => {
|
||||
window.location.href = `/song/${loaderData.id}/info`;
|
||||
}, 2000);
|
||||
}, 2000);
|
||||
}, 1200);
|
||||
}, 500);
|
||||
setImportInterval(interval);
|
||||
};
|
||||
|
||||
@ -106,9 +105,8 @@ export default function SongInfo({ loaderData }: Route.ComponentProps) {
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (importInterval) {
|
||||
clearInterval(importInterval);
|
||||
}
|
||||
if (!importInterval) return;
|
||||
clearInterval(importInterval);
|
||||
};
|
||||
}, [importInterval]);
|
||||
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { Layout } from "@/components/Layout";
|
||||
import type { Route } from "./+types/time-calculator";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { formatDateTime } from "@/components/SearchResults";
|
||||
|
||||
export default function Home() {
|
||||
@ -13,19 +15,108 @@ export default function Home() {
|
||||
const time2 = new Date(time2Input);
|
||||
const difference = time2.getTime() - time1.getTime();
|
||||
const days = Math.floor(difference / (1000 * 60 * 60 * 24));
|
||||
const hours = Math.floor(difference / (1000 * 60 * 60));
|
||||
const hours = Math.floor(difference / (1000 * 60 * 60)) % 24;
|
||||
const minutes = Math.floor((difference / (1000 * 60)) % 60);
|
||||
const diffString = `${days || 0} 天 ${hours || 0} 时 ${minutes || 0} 分`;
|
||||
const seconds = Math.floor((difference / 1000) % 60);
|
||||
|
||||
const diffString = `${Math.abs(days) || 0} 天 ${Math.abs(hours) || 0} 时 ${Math.abs(minutes) || 0} 分`;
|
||||
const isNegative = difference < 0;
|
||||
|
||||
const setQuickTime = (hoursOffset: number) => {
|
||||
const newTime = new Date(time1Input);
|
||||
newTime.setHours(newTime.getHours() + hoursOffset);
|
||||
setTime2Input(formatDateTime(newTime));
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<h1 className="my-5 text-2xl">时间计算器</h1>
|
||||
<p>在下方输入两个时间点,即可得到两个时间点之间的时间差</p>
|
||||
<div className="flex gap-5 mt-3">
|
||||
<Input className="text-center w-50" value={time1Input} onChange={(e) => setTime1Input(e.target.value)} />
|
||||
<Input className="text-center w-50" value={time2Input} onChange={(e) => setTime2Input(e.target.value)} />
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
<div className="space-y-2 mt-8">
|
||||
<h2 className="text-2xl font-bold tracking-tight">时间计算器</h2>
|
||||
<p className="text-muted-foreground">输入两个时间点,计算精确的时间差</p>
|
||||
</div>
|
||||
|
||||
{/* 时间输入区域 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>时间设置</CardTitle>
|
||||
<CardDescription>选择或输入开始时间和结束时间</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="start-time">开始时间</Label>
|
||||
<Input
|
||||
id="start-time"
|
||||
value={time1Input}
|
||||
onChange={(e) => setTime1Input(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setTime1Input(formatDateTime(now))}
|
||||
className="w-full"
|
||||
>
|
||||
设为当前时间
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="end-time">结束时间</Label>
|
||||
<Input
|
||||
id="end-time"
|
||||
value={time2Input}
|
||||
onChange={(e) => setTime2Input(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setTime2Input(formatDateTime(now))}
|
||||
className="w-full"
|
||||
>
|
||||
设为当前时间
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>计算结果</CardTitle>
|
||||
<CardDescription>两个时间点之间的精确时间差</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-center space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="text-2xl font-bold">{isNegative ? "时间差为负值" : diffString}</div>
|
||||
{isNegative && (
|
||||
<div className="text-lg text-muted-foreground">结束时间早于开始时间</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 pt-4">
|
||||
<div className="space-y-2">
|
||||
<div className="text-2xl font-bold">{Math.abs(days)}</div>
|
||||
<div className="text-sm text-muted-foreground">天数</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="text-2xl font-bold">{Math.abs(hours)}</div>
|
||||
<div className="text-sm text-muted-foreground">小时</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="text-2xl font-bold">{Math.abs(minutes)}</div>
|
||||
<div className="text-sm text-muted-foreground">分钟</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="text-2xl font-bold">{Math.abs(seconds)}</div>
|
||||
<div className="text-sm text-muted-foreground">秒数</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<p className="mt-3">{diffString}</p>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
"@nivo/line": "^0.99.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
"@radix-ui/react-progress": "^1.1.8",
|
||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user