feat: use command bar to search and focus source

This commit is contained in:
Ou 2024-10-28 21:28:13 +08:00
parent ae8cb37f7a
commit c3a825de74
20 changed files with 982 additions and 43 deletions

View File

@ -15,7 +15,7 @@
"dev": "vite dev", "dev": "vite dev",
"build": "vite build", "build": "vite build",
"lint": "eslint", "lint": "eslint",
"favicon": "tsx ./scripts/favicon.ts", "source": "tsx ./scripts/favicon.ts && tsx ./scripts/pinyin.ts",
"start": "node --env-file .env.server dist/output/server/index.mjs", "start": "node --env-file .env.server dist/output/server/index.mjs",
"preview": "CF_PAGES=1 pnpm run build && wrangler pages dev", "preview": "CF_PAGES=1 pnpm run build && wrangler pages dev",
"deploy": "CF_PAGES=1 pnpm run build && wrangler pages deploy", "deploy": "CF_PAGES=1 pnpm run build && wrangler pages deploy",
@ -36,6 +36,7 @@
"better-sqlite3": "^11.3.0", "better-sqlite3": "^11.3.0",
"cheerio": "^1.0.0", "cheerio": "^1.0.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.0.0",
"consola": "^3.2.3", "consola": "^3.2.3",
"cookie-es": "^1.2.2", "cookie-es": "^1.2.2",
"dayjs": "1.11.13", "dayjs": "1.11.13",
@ -60,6 +61,7 @@
"devDependencies": { "devDependencies": {
"@eslint-react/eslint-plugin": "^1.14.3", "@eslint-react/eslint-plugin": "^1.14.3",
"@iconify-json/ph": "^1.2.1", "@iconify-json/ph": "^1.2.1",
"@napi-rs/pinyin": "^1.7.5",
"@ourongxing/eslint-config": "3.2.3-beta.6", "@ourongxing/eslint-config": "3.2.3-beta.6",
"@ourongxing/tsconfig": "^0.0.4", "@ourongxing/tsconfig": "^0.0.4",
"@rollup/pluginutils": "^5.1.3", "@rollup/pluginutils": "^5.1.3",

598
pnpm-lock.yaml generated
View File

@ -47,6 +47,9 @@ importers:
clsx: clsx:
specifier: ^2.1.1 specifier: ^2.1.1
version: 2.1.1 version: 2.1.1
cmdk:
specifier: ^1.0.0
version: 1.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
consola: consola:
specifier: ^3.2.3 specifier: ^3.2.3
version: 3.2.3 version: 3.2.3
@ -114,6 +117,9 @@ importers:
'@iconify-json/ph': '@iconify-json/ph':
specifier: ^1.2.1 specifier: ^1.2.1
version: 1.2.1 version: 1.2.1
'@napi-rs/pinyin':
specifier: ^1.7.5
version: 1.7.5
'@ourongxing/eslint-config': '@ourongxing/eslint-config':
specifier: 3.2.3-beta.6 specifier: 3.2.3-beta.6
version: 3.2.3-beta.6(@eslint-react/eslint-plugin@1.15.0(eslint@9.13.0(jiti@2.3.3))(typescript@5.6.3))(@typescript-eslint/utils@8.11.0(eslint@9.13.0(jiti@2.3.3))(typescript@5.6.3))(@vue/compiler-sfc@3.5.12)(eslint-plugin-react-hooks@5.1.0-rc-fb9a90fa48-20240614(eslint@9.13.0(jiti@2.3.3)))(eslint-plugin-react-refresh@0.4.13(eslint@9.13.0(jiti@2.3.3)))(eslint@9.13.0(jiti@2.3.3))(typescript@5.6.3)(vitest@2.1.3(@types/node@22.7.9)(terser@5.36.0)) version: 3.2.3-beta.6(@eslint-react/eslint-plugin@1.15.0(eslint@9.13.0(jiti@2.3.3))(typescript@5.6.3))(@typescript-eslint/utils@8.11.0(eslint@9.13.0(jiti@2.3.3))(typescript@5.6.3))(@vue/compiler-sfc@3.5.12)(eslint-plugin-react-hooks@5.1.0-rc-fb9a90fa48-20240614(eslint@9.13.0(jiti@2.3.3)))(eslint-plugin-react-refresh@0.4.13(eslint@9.13.0(jiti@2.3.3)))(eslint@9.13.0(jiti@2.3.3))(typescript@5.6.3)(vitest@2.1.3(@types/node@22.7.9)(terser@5.36.0))
@ -1547,6 +1553,91 @@ packages:
resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==}
hasBin: true hasBin: true
'@napi-rs/pinyin-android-arm-eabi@1.7.5':
resolution: {integrity: sha512-dnurqJdedbU8D1Ngudf10nXvc4BbVSe4ki9U2LUZoGMDGa069t4c87BHRXvcy2By3YpOozORv9/QTMxAQFVOLg==}
engines: {node: '>= 10.0'}
cpu: [arm]
os: [android]
'@napi-rs/pinyin-android-arm64@1.7.5':
resolution: {integrity: sha512-75OFmFNpw3L1rDhs8rprG44inVSHouY0jaxL5a4WRHDErqE5PebDbpKUD3ATFUSENJV2iUFT4I9liP9PHOJHwQ==}
engines: {node: '>= 10.0'}
cpu: [arm64]
os: [android]
'@napi-rs/pinyin-darwin-arm64@1.7.5':
resolution: {integrity: sha512-DNtRvfiW7XLtTIv1iFYPAfmhFLoPqYFKs/LpElIuI7JKHpJIBfKs6/+9ln3WINji9cWz7lvW/XvT+DEpLVdSMA==}
engines: {node: '>= 10.0'}
cpu: [arm64]
os: [darwin]
'@napi-rs/pinyin-darwin-x64@1.7.5':
resolution: {integrity: sha512-h0WwbhdooPwaSIeYkMW7RDnr7tvECgQRR78nsDPxX6lQa6iqQvwWTlGpXBZC1AO6L2O+nJRriphwL95mUBYZ8w==}
engines: {node: '>= 10.0'}
cpu: [x64]
os: [darwin]
'@napi-rs/pinyin-freebsd-x64@1.7.5':
resolution: {integrity: sha512-9ImT5sYKkqiFAIJkmZBLzQsc/2X0/ne7Jf6ZmrD7xFh3YIbSvFhEPc23qKqW6HDKV2FgA3FDfZeT9ZhCAL3l+w==}
engines: {node: '>= 10.0'}
cpu: [x64]
os: [freebsd]
'@napi-rs/pinyin-linux-arm-gnueabihf@1.7.5':
resolution: {integrity: sha512-OslQ3DJDXCrd8hdXXUu2HWdb2o5bqTbOzRqvh5dp8gVkXlx/L8mUwBlDy62hxJkpkFxC7NaL8lJRzEvlpEdnBg==}
engines: {node: '>= 10.0'}
cpu: [arm]
os: [linux]
'@napi-rs/pinyin-linux-arm64-gnu@1.7.5':
resolution: {integrity: sha512-xGAB86BZFFmZ9BDRmAoZmsaxAGqBAek9+paGDze8/cIgIhK9rvNTfknyZjUQUeGXlESL5XuMsIp0VtjcOr6m5A==}
engines: {node: '>= 10.0'}
cpu: [arm64]
os: [linux]
'@napi-rs/pinyin-linux-arm64-musl@1.7.5':
resolution: {integrity: sha512-aK2VLMjde8/1AkoH+BqQ94r3w8YVdTCl5hJLI05GhhrNSz62ScrgPAsKNJmD9+8dcfexTdl/ORedAnenodnPzA==}
engines: {node: '>= 10.0'}
cpu: [arm64]
os: [linux]
'@napi-rs/pinyin-linux-x64-gnu@1.7.5':
resolution: {integrity: sha512-duTEnMo2m9H8AyQY32bM0OrLvZ14na6AH1DAD7/e3HX/TGow9p3LF1v4b8IlGeLZFZRyTdkWX6olHkolQ9fQkA==}
engines: {node: '>= 10.0'}
cpu: [x64]
os: [linux]
'@napi-rs/pinyin-linux-x64-musl@1.7.5':
resolution: {integrity: sha512-aZhzqTZh3VFwy5rs0LFEtpyqKwWZMWxrRYJk5awbwJg+nu4rSrFtv/9QbmfFhY5vJT2rc4ICabyxmSHngiNKog==}
engines: {node: '>= 10.0'}
cpu: [x64]
os: [linux]
'@napi-rs/pinyin-win32-arm64-msvc@1.7.5':
resolution: {integrity: sha512-qMlqDnRXM/dzt3KQKntZ53ea3E0vVXMWOp8EmyQaXhppkaPUBurCKsEYVNu4tO3R9LWFv6crT02bs7uTu1eUHw==}
engines: {node: '>= 10.0'}
cpu: [arm64]
os: [win32]
'@napi-rs/pinyin-win32-ia32-msvc@1.7.5':
resolution: {integrity: sha512-lmpmY6FM4SymqsDM+1v0469Wpjc1CGvUUedMDzoqHAmdKHaxl4Db95km/DrvvFb9SPWFx6Gma1I8BbztJRlzeA==}
engines: {node: '>= 10.0'}
cpu: [ia32]
os: [win32]
'@napi-rs/pinyin-win32-x64-msvc@1.7.5':
resolution: {integrity: sha512-0GrSIMZ4kVJzj3/hG76t4mPPLQbiQbQZk8AEBbFNakNnyoms6aCfbmxAG1qoqs1o+M+TAEJlW6mmI6VmSPqTfg==}
engines: {node: '>= 10.0'}
cpu: [x64]
os: [win32]
'@napi-rs/pinyin@1.7.5':
resolution: {integrity: sha512-RaONcl+ue1xt31ScGajc1SJ45TQcyEuNugAZ9ZDsJ+EgjnvVpw0qpD7VvK8+E8Nvc1VW3BfdTF0Ej+zIBtwHSw==}
engines: {node: '>= 10.0'}
'@napi-rs/triples@1.2.0':
resolution: {integrity: sha512-HAPjR3bnCsdXBsATpDIP5WCrw0JcACwhhrwIAQhiR46n+jm+a2F8kBsfseAuWtSyQ+H3Yebt2k43B5dy+04yMA==}
'@netlify/functions@2.8.2': '@netlify/functions@2.8.2':
resolution: {integrity: sha512-DeoAQh8LuNPvBE4qsKlezjKj0PyXDryOFJfJKo3Z1qZLKzQ21sT314KQKPVjfvw6knqijj+IO+0kHXy/TJiqNA==} resolution: {integrity: sha512-DeoAQh8LuNPvBE4qsKlezjKj0PyXDryOFJfJKo3Z1qZLKzQ21sT314KQKPVjfvw6knqijj+IO+0kHXy/TJiqNA==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
@ -1713,6 +1804,168 @@ packages:
'@polka/url@1.0.0-next.28': '@polka/url@1.0.0-next.28':
resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==} resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==}
'@radix-ui/primitive@1.0.1':
resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==}
'@radix-ui/react-compose-refs@1.0.1':
resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-context@1.0.1':
resolution: {integrity: sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-dialog@1.0.5':
resolution: {integrity: sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-dismissable-layer@1.0.5':
resolution: {integrity: sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-focus-guards@1.0.1':
resolution: {integrity: sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-focus-scope@1.0.4':
resolution: {integrity: sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-id@1.0.1':
resolution: {integrity: sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-portal@1.0.4':
resolution: {integrity: sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-presence@1.0.1':
resolution: {integrity: sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-primitive@1.0.3':
resolution: {integrity: sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-slot@1.0.2':
resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-callback-ref@1.0.1':
resolution: {integrity: sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-controllable-state@1.0.1':
resolution: {integrity: sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-escape-keydown@1.0.3':
resolution: {integrity: sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-layout-effect@1.0.1':
resolution: {integrity: sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@redocly/ajv@8.11.2': '@redocly/ajv@8.11.2':
resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==} resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==}
@ -2445,6 +2698,10 @@ packages:
argparse@2.0.1: argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
aria-hidden@1.2.4:
resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==}
engines: {node: '>=10'}
array-buffer-byte-length@1.0.1: array-buffer-byte-length@1.0.1:
resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -2698,6 +2955,12 @@ packages:
resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
cmdk@1.0.0:
resolution: {integrity: sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==}
peerDependencies:
react: ^18.0.0
react-dom: ^18.0.0
color-convert@1.9.3: color-convert@1.9.3:
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
@ -2958,6 +3221,9 @@ packages:
resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==}
engines: {node: '>=8'} engines: {node: '>=8'}
detect-node-es@1.1.0:
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
doctrine@3.0.0: doctrine@3.0.0:
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
@ -3570,6 +3836,10 @@ packages:
resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
get-nonce@1.0.1:
resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
engines: {node: '>=6'}
get-own-enumerable-property-symbols@3.0.2: get-own-enumerable-property-symbols@3.0.2:
resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==} resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==}
@ -3800,6 +4070,9 @@ packages:
resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
invariant@2.2.4:
resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
ioredis@5.4.1: ioredis@5.4.1:
resolution: {integrity: sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==} resolution: {integrity: sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==}
engines: {node: '>=12.22.0'} engines: {node: '>=12.22.0'}
@ -4735,6 +5008,36 @@ packages:
peerDependencies: peerDependencies:
react: ^18.3.1 react: ^18.3.1
react-remove-scroll-bar@2.3.6:
resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
react-remove-scroll@2.5.5:
resolution: {integrity: sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
react-style-singleton@2.2.1:
resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
react-universal-interface@0.6.2: react-universal-interface@0.6.2:
resolution: {integrity: sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==} resolution: {integrity: sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==}
peerDependencies: peerDependencies:
@ -5622,6 +5925,26 @@ packages:
urlpattern-polyfill@8.0.2: urlpattern-polyfill@8.0.2:
resolution: {integrity: sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==} resolution: {integrity: sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==}
use-callback-ref@1.3.2:
resolution: {integrity: sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
use-sidecar@1.1.2:
resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': ^16.9.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
use-sync-external-store@1.2.2: use-sync-external-store@1.2.2:
resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==} resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==}
peerDependencies: peerDependencies:
@ -7244,6 +7567,65 @@ snapshots:
- encoding - encoding
- supports-color - supports-color
'@napi-rs/pinyin-android-arm-eabi@1.7.5':
optional: true
'@napi-rs/pinyin-android-arm64@1.7.5':
optional: true
'@napi-rs/pinyin-darwin-arm64@1.7.5':
optional: true
'@napi-rs/pinyin-darwin-x64@1.7.5':
optional: true
'@napi-rs/pinyin-freebsd-x64@1.7.5':
optional: true
'@napi-rs/pinyin-linux-arm-gnueabihf@1.7.5':
optional: true
'@napi-rs/pinyin-linux-arm64-gnu@1.7.5':
optional: true
'@napi-rs/pinyin-linux-arm64-musl@1.7.5':
optional: true
'@napi-rs/pinyin-linux-x64-gnu@1.7.5':
optional: true
'@napi-rs/pinyin-linux-x64-musl@1.7.5':
optional: true
'@napi-rs/pinyin-win32-arm64-msvc@1.7.5':
optional: true
'@napi-rs/pinyin-win32-ia32-msvc@1.7.5':
optional: true
'@napi-rs/pinyin-win32-x64-msvc@1.7.5':
optional: true
'@napi-rs/pinyin@1.7.5':
dependencies:
'@napi-rs/triples': 1.2.0
optionalDependencies:
'@napi-rs/pinyin-android-arm-eabi': 1.7.5
'@napi-rs/pinyin-android-arm64': 1.7.5
'@napi-rs/pinyin-darwin-arm64': 1.7.5
'@napi-rs/pinyin-darwin-x64': 1.7.5
'@napi-rs/pinyin-freebsd-x64': 1.7.5
'@napi-rs/pinyin-linux-arm-gnueabihf': 1.7.5
'@napi-rs/pinyin-linux-arm64-gnu': 1.7.5
'@napi-rs/pinyin-linux-arm64-musl': 1.7.5
'@napi-rs/pinyin-linux-x64-gnu': 1.7.5
'@napi-rs/pinyin-linux-x64-musl': 1.7.5
'@napi-rs/pinyin-win32-arm64-msvc': 1.7.5
'@napi-rs/pinyin-win32-ia32-msvc': 1.7.5
'@napi-rs/pinyin-win32-x64-msvc': 1.7.5
'@napi-rs/triples@1.2.0': {}
'@netlify/functions@2.8.2': '@netlify/functions@2.8.2':
dependencies: dependencies:
'@netlify/serverless-functions-api': 1.26.1 '@netlify/serverless-functions-api': 1.26.1
@ -7387,6 +7769,157 @@ snapshots:
'@polka/url@1.0.0-next.28': {} '@polka/url@1.0.0-next.28': {}
'@radix-ui/primitive@1.0.1':
dependencies:
'@babel/runtime': 7.25.9
'@radix-ui/react-compose-refs@1.0.1(@types/react@18.3.12)(react@18.3.1)':
dependencies:
'@babel/runtime': 7.25.9
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.12
'@radix-ui/react-context@1.0.1(@types/react@18.3.12)(react@18.3.1)':
dependencies:
'@babel/runtime': 7.25.9
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.12
'@radix-ui/react-dialog@1.0.5(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@babel/runtime': 7.25.9
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.12)(react@18.3.1)
'@radix-ui/react-context': 1.0.1(@types/react@18.3.12)(react@18.3.1)
'@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-focus-guards': 1.0.1(@types/react@18.3.12)(react@18.3.1)
'@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-id': 1.0.1(@types/react@18.3.12)(react@18.3.1)
'@radix-ui/react-portal': 1.0.4(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-presence': 1.0.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-slot': 1.0.2(@types/react@18.3.12)(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.12)(react@18.3.1)
aria-hidden: 1.2.4
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-remove-scroll: 2.5.5(@types/react@18.3.12)(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.12
'@types/react-dom': 18.3.1
'@radix-ui/react-dismissable-layer@1.0.5(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@babel/runtime': 7.25.9
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.12)(react@18.3.1)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.12)(react@18.3.1)
'@radix-ui/react-use-escape-keydown': 1.0.3(@types/react@18.3.12)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.12
'@types/react-dom': 18.3.1
'@radix-ui/react-focus-guards@1.0.1(@types/react@18.3.12)(react@18.3.1)':
dependencies:
'@babel/runtime': 7.25.9
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.12
'@radix-ui/react-focus-scope@1.0.4(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@babel/runtime': 7.25.9
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.12)(react@18.3.1)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.12)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.12
'@types/react-dom': 18.3.1
'@radix-ui/react-id@1.0.1(@types/react@18.3.12)(react@18.3.1)':
dependencies:
'@babel/runtime': 7.25.9
'@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.12)(react@18.3.1)
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.12
'@radix-ui/react-portal@1.0.4(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@babel/runtime': 7.25.9
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.12
'@types/react-dom': 18.3.1
'@radix-ui/react-presence@1.0.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@babel/runtime': 7.25.9
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.12)(react@18.3.1)
'@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.12)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.12
'@types/react-dom': 18.3.1
'@radix-ui/react-primitive@1.0.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@babel/runtime': 7.25.9
'@radix-ui/react-slot': 1.0.2(@types/react@18.3.12)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.12
'@types/react-dom': 18.3.1
'@radix-ui/react-slot@1.0.2(@types/react@18.3.12)(react@18.3.1)':
dependencies:
'@babel/runtime': 7.25.9
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.12)(react@18.3.1)
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.12
'@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.3.12)(react@18.3.1)':
dependencies:
'@babel/runtime': 7.25.9
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.12
'@radix-ui/react-use-controllable-state@1.0.1(@types/react@18.3.12)(react@18.3.1)':
dependencies:
'@babel/runtime': 7.25.9
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.12)(react@18.3.1)
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.12
'@radix-ui/react-use-escape-keydown@1.0.3(@types/react@18.3.12)(react@18.3.1)':
dependencies:
'@babel/runtime': 7.25.9
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.12)(react@18.3.1)
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.12
'@radix-ui/react-use-layout-effect@1.0.1(@types/react@18.3.12)(react@18.3.1)':
dependencies:
'@babel/runtime': 7.25.9
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.12
'@redocly/ajv@8.11.2': '@redocly/ajv@8.11.2':
dependencies: dependencies:
fast-deep-equal: 3.1.3 fast-deep-equal: 3.1.3
@ -8267,6 +8800,10 @@ snapshots:
argparse@2.0.1: {} argparse@2.0.1: {}
aria-hidden@1.2.4:
dependencies:
tslib: 2.8.0
array-buffer-byte-length@1.0.1: array-buffer-byte-length@1.0.1:
dependencies: dependencies:
call-bind: 1.0.7 call-bind: 1.0.7
@ -8587,6 +9124,16 @@ snapshots:
cluster-key-slot@1.1.2: {} cluster-key-slot@1.1.2: {}
cmdk@1.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
- '@types/react-dom'
color-convert@1.9.3: color-convert@1.9.3:
dependencies: dependencies:
color-name: 1.1.3 color-name: 1.1.3
@ -8784,6 +9331,8 @@ snapshots:
detect-libc@2.0.3: {} detect-libc@2.0.3: {}
detect-node-es@1.1.0: {}
doctrine@3.0.0: doctrine@3.0.0:
dependencies: dependencies:
esutils: 2.0.3 esutils: 2.0.3
@ -9663,6 +10212,8 @@ snapshots:
has-symbols: 1.0.3 has-symbols: 1.0.3
hasown: 2.0.2 hasown: 2.0.2
get-nonce@1.0.1: {}
get-own-enumerable-property-symbols@3.0.2: {} get-own-enumerable-property-symbols@3.0.2: {}
get-port-please@3.1.2: {} get-port-please@3.1.2: {}
@ -9924,6 +10475,10 @@ snapshots:
hasown: 2.0.2 hasown: 2.0.2
side-channel: 1.0.6 side-channel: 1.0.6
invariant@2.2.4:
dependencies:
loose-envify: 1.4.0
ioredis@5.4.1: ioredis@5.4.1:
dependencies: dependencies:
'@ioredis/commands': 1.2.0 '@ioredis/commands': 1.2.0
@ -10914,6 +11469,34 @@ snapshots:
react: 18.3.1 react: 18.3.1
scheduler: 0.23.2 scheduler: 0.23.2
react-remove-scroll-bar@2.3.6(@types/react@18.3.12)(react@18.3.1):
dependencies:
react: 18.3.1
react-style-singleton: 2.2.1(@types/react@18.3.12)(react@18.3.1)
tslib: 2.8.0
optionalDependencies:
'@types/react': 18.3.12
react-remove-scroll@2.5.5(@types/react@18.3.12)(react@18.3.1):
dependencies:
react: 18.3.1
react-remove-scroll-bar: 2.3.6(@types/react@18.3.12)(react@18.3.1)
react-style-singleton: 2.2.1(@types/react@18.3.12)(react@18.3.1)
tslib: 2.8.0
use-callback-ref: 1.3.2(@types/react@18.3.12)(react@18.3.1)
use-sidecar: 1.1.2(@types/react@18.3.12)(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.12
react-style-singleton@2.2.1(@types/react@18.3.12)(react@18.3.1):
dependencies:
get-nonce: 1.0.1
invariant: 2.2.4
react: 18.3.1
tslib: 2.8.0
optionalDependencies:
'@types/react': 18.3.12
react-universal-interface@0.6.2(react@18.3.1)(tslib@2.8.0): react-universal-interface@0.6.2(react@18.3.1)(tslib@2.8.0):
dependencies: dependencies:
react: 18.3.1 react: 18.3.1
@ -11883,6 +12466,21 @@ snapshots:
urlpattern-polyfill@8.0.2: {} urlpattern-polyfill@8.0.2: {}
use-callback-ref@1.3.2(@types/react@18.3.12)(react@18.3.1):
dependencies:
react: 18.3.1
tslib: 2.8.0
optionalDependencies:
'@types/react': 18.3.12
use-sidecar@1.1.2(@types/react@18.3.12)(react@18.3.1):
dependencies:
detect-node-es: 1.1.0
react: 18.3.1
tslib: 2.8.0
optionalDependencies:
'@types/react': 18.3.12
use-sync-external-store@1.2.2(react@18.3.1): use-sync-external-store@1.2.2(react@18.3.1):
dependencies: dependencies:
react: 18.3.1 react: 18.3.1

13
scripts/pinyin.ts Normal file
View File

@ -0,0 +1,13 @@
import { writeFileSync } from "node:fs"
import { join } from "node:path"
import { pinyin } from "@napi-rs/pinyin"
import { projectDir } from "../shared/dir"
import { sources } from "../shared/sources"
const pinyinMap = Object.fromEntries(Object.entries(sources)
.filter(([, v]) => !v.redirect)
.map(([k, v]) => {
return [k, pinyin(v.title ? `${v.name}-${v.title}` : v.name).join("")]
}))
writeFileSync(join(projectDir, "./shared/pinyin.json"), JSON.stringify(pinyinMap, undefined, 2))

View File

@ -30,6 +30,7 @@ export default defineEventHandler(async (event): Promise<SourceResponse> => {
if (now - cache.updated < interval) { if (now - cache.updated < interval) {
return { return {
status: "success", status: "success",
id,
updatedTime: now, updatedTime: now,
items: cache.data, items: cache.data,
} }
@ -46,6 +47,7 @@ export default defineEventHandler(async (event): Promise<SourceResponse> => {
if (!latest || (!event.context.disabledLogin && !event.context.user)) { if (!latest || (!event.context.disabledLogin && !event.context.user)) {
return { return {
status: "cache", status: "cache",
id,
updatedTime: cache.updated, updatedTime: cache.updated,
items: cache.data, items: cache.data,
} }
@ -62,6 +64,7 @@ export default defineEventHandler(async (event): Promise<SourceResponse> => {
} }
return { return {
status: "success", status: "success",
id,
updatedTime: now, updatedTime: now,
items: data, items: data,
} }

View File

@ -1,10 +1,8 @@
import { sources } from "./sources" import { sources } from "./sources"
import { typeSafeObjectEntries, typeSafeObjectFromEntries } from "./type.util" import { typeSafeObjectEntries, typeSafeObjectFromEntries } from "./type.util"
import type { ColumnID, Metadata, SourceID } from "./types" import type { ColumnID, HiddenColumnID, Metadata, SourceID } from "./types"
export const columnIds = ["focus", "realtime", "hottest", "china", "world", "tech", "finance"] as const export const columns = {
const columnName: Record<ColumnID, { zh: string }> = {
china: { china: {
zh: "国内", zh: "国内",
}, },
@ -26,9 +24,12 @@ const columnName: Record<ColumnID, { zh: string }> = {
hottest: { hottest: {
zh: "最热", zh: "最热",
}, },
} } as const
export const metadata: Metadata = typeSafeObjectFromEntries(typeSafeObjectEntries(columnName).map(([k, v]) => { export const fixedColumnIds = ["focus", "hottest", "realtime"] as const satisfies Partial<ColumnID>[]
export const hiddenColumns = Object.keys(sources).filter(id => !fixedColumnIds.includes(id as any)) as HiddenColumnID[]
export const metadata: Metadata = typeSafeObjectFromEntries(typeSafeObjectEntries(columns).map(([k, v]) => {
switch (k) { switch (k) {
case "focus": case "focus":
return [k, { return [k, {

30
shared/pinyin.json Normal file
View File

@ -0,0 +1,30 @@
{
"v2ex-share": "V2EX-zuixinfenxiang",
"zhihu": "zhihu",
"weibo": "weibo-shishiresou",
"zaobao": "lianhezaobao",
"coolapk": "kuan-jinrizuire",
"wallstreetcn-quick": "huaerjiejianwen-shishikuaixun",
"wallstreetcn-news": "huaerjiejianwen-zuixinzixun",
"wallstreetcn-hot": "huaerjiejianwen-zuirewenzhang",
"douyin": "douyin",
"tieba": "baidutieba-reyi",
"toutiao": "jinritoutiao",
"ithome": "ITzhijia",
"thepaper": "pengpaixinwen-rebang",
"cankaoxiaoxi": "cankaoxiaoxi",
"cls-telegraph": "cailianshe-dianbao",
"cls-depth": "cailianshe-shendutoutiao",
"xueqiu-hotstock": "xueqiu-remengupiao",
"gelonghui": "gelonghui-shijian",
"fastbull-express": "fabucaijing-kuaixun",
"fastbull-news": "fabucaijing-toutiao",
"solidot": "Solidot",
"hackernews": "Hacker News",
"producthunt": "Product Hunt",
"github-trending-today": "Github-Today",
"bilibili-hot-search": "bilibili-resou",
"kaopu": "kaopuxinwen",
"jin10": "jinshishuju",
"baidu": "baiduresou"
}

View File

@ -1,5 +1,5 @@
import type { colors } from "unocss/preset-mini" import type { colors } from "unocss/preset-mini"
import type { columnIds } from "./metadata" import type { columns, fixedColumnIds } from "./metadata"
import type { originSources } from "./sources" import type { originSources } from "./sources"
export type Color = "primary" | Exclude<keyof typeof colors, "current" | "inherit" | "transparent" | "black" | "white"> export type Color = "primary" | Exclude<keyof typeof colors, "current" | "inherit" | "transparent" | "black" | "white">
@ -24,16 +24,17 @@ export type AllSourceID = {
// export type DisabledSourceID = Exclude<SourceID, MainSourceID> // export type DisabledSourceID = Exclude<SourceID, MainSourceID>
export type ColumnID = (typeof columnIds)[number] export type ColumnID = keyof typeof columns
export type Metadata = Record<ColumnID, Column> export type Metadata = Record<ColumnID, Column>
export interface PrimitiveMetadata { export interface PrimitiveMetadata {
updatedTime: number updatedTime: number
data: Record<ColumnID, SourceID[]> data: Record<FixedColumnID, SourceID[]>
action: "init" | "manual" | "sync" action: "init" | "manual" | "sync"
} }
type ManualColumnID = Exclude<ColumnID, "focus" | "realtime" | "hottest"> export type FixedColumnID = (typeof fixedColumnIds)[number]
export type HiddenColumnID = Exclude<ColumnID, FixedColumnID>
export interface OriginSource extends Partial<Omit<Source, "name" | "redirect">> { export interface OriginSource extends Partial<Omit<Source, "name" | "redirect">> {
name: string name: string
@ -69,7 +70,7 @@ export interface Source {
* Default normal timeline * Default normal timeline
*/ */
type?: "hottest" | "realtime" type?: "hottest" | "realtime"
column?: ManualColumnID column?: HiddenColumnID
home?: string home?: string
/** /**
* @default false * @default false
@ -103,6 +104,7 @@ export interface NewsItem {
export interface SourceResponse { export interface SourceResponse {
status: "success" | "cache" status: "success" | "cache"
id: SourceID
updatedTime: number | string updatedTime: number | string
items: NewsItem[] items: NewsItem[]
} }

View File

@ -1,8 +1,8 @@
import { atom } from "jotai" import { atom } from "jotai"
import type { ColumnID, SourceID } from "@shared/types" import type { FixedColumnID, SourceID } from "@shared/types"
import { sources } from "@shared/sources" import { sources } from "@shared/sources"
import { primitiveMetadataAtom } from "./primitiveMetadataAtom" import { primitiveMetadataAtom } from "./primitiveMetadataAtom"
import type { ToastItem, Update } from "./types" import type { Update } from "./types"
export { primitiveMetadataAtom, preprocessMetadata } from "./primitiveMetadataAtom" export { primitiveMetadataAtom, preprocessMetadata } from "./primitiveMetadataAtom"
@ -35,7 +35,7 @@ function initRefetchSources() {
export const refetchSourcesAtom = atom(initRefetchSources()) export const refetchSourcesAtom = atom(initRefetchSources())
export const currentColumnIDAtom = atom<ColumnID>("focus") export const currentColumnIDAtom = atom<FixedColumnID>("focus")
export const currentSourcesAtom = atom((get) => { export const currentSourcesAtom = atom((get) => {
const id = get(currentColumnIDAtom) const id = get(currentColumnIDAtom)
@ -56,5 +56,3 @@ export const goToTopAtom = atom({
ok: false, ok: false,
fn: undefined as (() => void) | undefined, fn: undefined as (() => void) | undefined,
}) })
export const toastAtom = atom<ToastItem[]>([])

View File

@ -1,8 +1,8 @@
import { metadata } from "@shared/metadata" import { fixedColumnIds, metadata } from "@shared/metadata"
import { typeSafeObjectEntries, typeSafeObjectFromEntries } from "@shared/type.util" import { typeSafeObjectEntries, typeSafeObjectFromEntries } from "@shared/type.util"
import type { PrimitiveAtom } from "jotai" import type { PrimitiveAtom } from "jotai"
import { atom } from "jotai" import { atom } from "jotai"
import type { ColumnID, PrimitiveMetadata, SourceID } from "@shared/types" import type { FixedColumnID, PrimitiveMetadata, SourceID } from "@shared/types"
import { verifyPrimitiveMetadata } from "@shared/verify" import { verifyPrimitiveMetadata } from "@shared/verify"
import { sources } from "@shared/sources" import { sources } from "@shared/sources"
import type { Update } from "./types" import type { Update } from "./types"
@ -37,7 +37,9 @@ function createPrimitiveMetadataAtom(
return derivedAtom return derivedAtom
} }
const initialMetadata = typeSafeObjectFromEntries(typeSafeObjectEntries(metadata).map(([id, val]) => [id, val.sources] as [ColumnID, SourceID[]])) const initialMetadata = typeSafeObjectFromEntries(typeSafeObjectEntries(metadata)
.filter(([id]) => fixedColumnIds.includes(id as any))
.map(([id, val]) => [id, val.sources] as [FixedColumnID, SourceID[]]))
export function preprocessMetadata(target: PrimitiveMetadata) { export function preprocessMetadata(target: PrimitiveMetadata) {
return { return {
data: { data: {

View File

@ -9,9 +9,10 @@ import type { SyntheticListenerMap } from "@dnd-kit/core/dist/hooks/utilities"
import { ofetch } from "ofetch" import { ofetch } from "ofetch"
import { useWindowSize } from "react-use" import { useWindowSize } from "react-use"
import { OverlayScrollbar } from "../common/overlay-scrollbar" import { OverlayScrollbar } from "../common/overlay-scrollbar"
import { focusSourcesAtom, refetchSourcesAtom } from "~/atoms" import { refetchSourcesAtom } from "~/atoms"
import { useRelativeTime } from "~/hooks/useRelativeTime" import { useRelativeTime } from "~/hooks/useRelativeTime"
import { safeParseString } from "~/utils" import { safeParseString } from "~/utils"
import { useFocusWith } from "~/hooks/useFocus"
export interface ItemsProps extends React.HTMLAttributes<HTMLDivElement> { export interface ItemsProps extends React.HTMLAttributes<HTMLDivElement> {
id: SourceID id: SourceID
@ -56,7 +57,6 @@ export const CardWrapper = forwardRef<HTMLDivElement, ItemsProps>(({ id, isDragg
const prevSourceItems: Partial<Record<SourceID, NewsItem[]>> = {} const prevSourceItems: Partial<Record<SourceID, NewsItem[]>> = {}
function NewsCard({ id, inView, handleListeners }: NewsCardProps) { function NewsCard({ id, inView, handleListeners }: NewsCardProps) {
const [focusSources, setFocusSources] = useAtom(focusSourcesAtom)
const [refetchSource, setRefetchSource] = useAtom(refetchSourcesAtom) const [refetchSource, setRefetchSource] = useAtom(refetchSourcesAtom)
const { data, isFetching, isPlaceholderData, isError } = useQuery({ const { data, isFetching, isPlaceholderData, isError } = useQuery({
queryKey: [id, refetchSource[id]], queryKey: [id, refetchSource[id]],
@ -92,16 +92,15 @@ function NewsCard({ id, inView, handleListeners }: NewsCardProps) {
}, },
// refetch 时显示原有的数据 // refetch 时显示原有的数据
placeholderData: (prev) => { placeholderData: (prev) => {
if (prev?.items && sources[id].type === "hottest") prevSourceItems[id] = prev.items if (prev?.id === id) {
return prev if (prev?.items && sources[id].type === "hottest") prevSourceItems[id] = prev.items
return prev
}
}, },
staleTime: 1000 * 60 * 5, staleTime: 1000 * 60 * 5,
enabled: inView, enabled: inView,
}) })
const addFocusList = useCallback(() => {
setFocusSources(focusSources.includes(id) ? focusSources.filter(i => i !== id) : [...focusSources, id])
}, [setFocusSources, focusSources, id])
const manualRefetch = useCallback(() => { const manualRefetch = useCallback(() => {
setRefetchSource(prev => ({ setRefetchSource(prev => ({
...prev, ...prev,
@ -111,12 +110,15 @@ function NewsCard({ id, inView, handleListeners }: NewsCardProps) {
const isFreshFetching = useMemo(() => isFetching && !isPlaceholderData, [isFetching, isPlaceholderData]) const isFreshFetching = useMemo(() => isFetching && !isPlaceholderData, [isFetching, isPlaceholderData])
const { isFocused, toggleFocus } = useFocusWith(id)
return ( return (
<> <>
<div className={clsx("flex justify-between mx-2 mt-0 mb-2 items-center")}> <div className={clsx("flex justify-between mx-2 mt-0 mb-2 items-center")}>
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<a <a
className={clsx("w-8 h-8 rounded-full bg-cover hover:animate-spin")} className={clsx("w-8 h-8 rounded-full bg-cover hover:animate-spin")}
target="_blank"
href={sources[id].home} href={sources[id].home}
title={sources[id].desc} title={sources[id].desc}
style={{ style={{
@ -144,14 +146,16 @@ function NewsCard({ id, inView, handleListeners }: NewsCardProps) {
/> />
<button <button
type="button" type="button"
className={clsx("btn", focusSources.includes(id) ? "i-ph:star-fill" : "i-ph:star-duotone")} className={clsx("btn", isFocused ? "i-ph:star-fill" : "i-ph:star-duotone")}
onClick={addFocusList} onClick={toggleFocus}
/>
<button
{...handleListeners}
type="button"
className={clsx("btn", "i-ph:dots-six-vertical-duotone", "cursor-grab")}
/> />
{handleListeners && (
<button
{...handleListeners}
type="button"
className={clsx("btn", "i-ph:dots-six-vertical-duotone", "cursor-grab")}
/>
)}
</div> </div>
</div> </div>

View File

@ -0,0 +1,77 @@
[data-radix-focus-guard] {
background-color: black;
}
[cmdk-item] {
--at-apply: p-1 mb-1 rounded-md;
}
[cmdk-item]:hover {
--at-apply: bg-neutral-400/10;
}
[cmdk-item][data-selected=true] {
--at-apply: bg-neutral-400/20;
}
[cmdk-input]{
--at-apply: w-full p-3 outline-none bg-transparent placeholder:color-neutral-500/60 border-color-neutral/10 border-b;
}
[cmdk-list] {
--at-apply: px-3 flex flex-col gap-2 items-stretch h-400px;
}
[cmdk-group-heading] {
--at-apply: text-sm font-bold op-70 ml-1 my-2;
}
[cmdk-dialog] {
--at-apply: bg-base sprinkle-primary bg-op-97 backdrop-blur-5 shadow pb-4 rounded-2xl shadow-2xl relative outline-none;
position: fixed;
width: 80vw ;
max-width: 675px;
z-index: 999;
left: 50%;
top: 50%;
transform: translateX(-50%) translateY(-50%);
}
[cmdk-dialog] {
transition: opacity;
transform-origin: center center;
animation: dialogIn 0.3s forwards
}
[cmdk-dialog][data-state=closed]{
animation: dialogOut 0.2s forwards
}
@keyframes dialogIn{
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes dialogOut {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
[cmdk-empty] {
--at-apply: flex justify-center items-center text-sm whitespace-pre-wrap op-70;
}
[cmdk-overlay] {
--at-apply: fixed inset-0 bg-black bg-op-50;
}

View File

@ -0,0 +1,142 @@
import { Command } from "cmdk"
import { useMount } from "react-use"
import type { SourceID } from "@shared/types"
import { useMemo, useRef, useState } from "react"
import { sources } from "@shared/sources"
import clsx from "clsx"
import { typeSafeObjectEntries } from "@shared/type.util"
import pinyin from "@shared/pinyin.json"
import { columns } from "@shared/metadata"
import { OverlayScrollbar } from "../overlay-scrollbar"
import { useSearchBar } from "~/hooks/useSearch"
import { CardWrapper } from "~/components/column/card"
import { useFocusWith } from "~/hooks/useFocus"
import "./cmdk.css"
interface SourceItemProps {
id: SourceID
name: string
title?: string
column: any
pinyin: string
}
function groupByColumn(items: SourceItemProps[]) {
return items.reduce((acc, item) => {
const k = acc.find(i => i.column === item.column)
if (k) k.sources = [...k.sources, item]
else acc.push({ column: item.column, sources: [item] })
return acc
}, [] as {
column: string
sources: SourceItemProps[]
}[]).sort((m, n) => {
if (m.column === "科技") return -1
if (n.column === "科技") return 1
if (m.column === "未分类") return 1
if (n.column === "未分类") return -1
return m.column < n.column ? -1 : 1
})
}
export function SearchBar() {
const { opened, toggle } = useSearchBar()
const sourceItems = useMemo(
() =>
groupByColumn(typeSafeObjectEntries(sources)
.filter(([_, source]) => !source.redirect)
.map(([k, source]) => ({
id: k,
title: source.title,
column: source.column ? columns[source.column].zh : "未分类",
name: source.name,
pinyin: pinyin?.[k as keyof typeof pinyin],
})))
, [],
)
const inputRef = useRef<HTMLInputElement | null>(null)
const [value, setValue] = useState<SourceID>("github-trending-today")
useMount(() => {
inputRef?.current?.focus()
const keydown = (e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
toggle()
}
}
document.addEventListener("keydown", keydown)
return () => {
document.removeEventListener("keydown", keydown)
}
})
return (
<Command.Dialog
open={opened}
onOpenChange={toggle}
value={value}
onValueChange={(v) => {
if (v in sources) {
setValue(v as SourceID)
}
}}
>
<Command.Input
ref={inputRef}
autoFocus
placeholder="搜索你想要的"
/>
<div className="md:flex pt-2">
<OverlayScrollbar defer={false} className="overflow-y-auto md:min-w-275px">
<Command.List>
<Command.Empty> Github issue </Command.Empty>
{
sourceItems.map(({ column, sources }) => (
<Command.Group heading={column} key={column}>
{
sources.map(item => <SourceItem item={item} key={item.id} />)
}
</Command.Group>
),
)
}
</Command.List>
</OverlayScrollbar>
<div className="flex-1 pt-2 px-4 min-w-350px max-md:hidden">
<CardWrapper id={value} />
</div>
</div>
</Command.Dialog>
)
}
function SourceItem({ item }: {
item: SourceItemProps
}) {
const { isFocused, toggleFocus } = useFocusWith(item.id)
return (
<Command.Item
keywords={[item.name, item.title ?? "", item.pinyin]}
value={item.id}
className="flex justify-between items-center p-2"
onSelect={toggleFocus}
>
<span className="flex gap-2 items-center">
<span
className={clsx("w-4 h-4 rounded-md bg-cover")}
style={{
backgroundImage: `url(/icons/${item.id.split("-")[0]}.png)`,
}}
/>
<span>{item.name}</span>
<span className="text-xs text-neutral-400/80 self-end mb-3px">{item.title}</span>
</span>
<span className={clsx(isFocused ? "i-ph-star-fill" : "i-ph-star-duotone", "bg-primary op-40")}></span>
</Command.Item>
)
}

View File

@ -3,8 +3,8 @@ import { AnimatePresence, motion } from "framer-motion"
import { useAtomValue, useSetAtom } from "jotai" import { useAtomValue, useSetAtom } from "jotai"
import { useCallback, useMemo, useRef } from "react" import { useCallback, useMemo, useRef } from "react"
import { useHoverDirty, useMount, useUpdateEffect, useWindowSize } from "react-use" import { useHoverDirty, useMount, useUpdateEffect, useWindowSize } from "react-use"
import { toastAtom } from "~/atoms"
import type { ToastItem } from "~/atoms/types" import type { ToastItem } from "~/atoms/types"
import { toastAtom } from "~/hooks/useToast"
import { Timer } from "~/utils" import { Timer } from "~/utils"
const WIDTH = 320 const WIDTH = 320

View File

@ -8,7 +8,14 @@ import { Homepage, Version } from "@shared/consts"
import { NavBar } from "../navbar" import { NavBar } from "../navbar"
import { Menu } from "./menu" import { Menu } from "./menu"
import { currentSourcesAtom, goToTopAtom, refetchSourcesAtom } from "~/atoms" import { currentSourcesAtom, goToTopAtom, refetchSourcesAtom } from "~/atoms"
import { useSearchBar } from "~/hooks/useSearch"
export function Search() {
const { toggle } = useSearchBar()
return (
<button type="button" className="i-ph:magnifying-glass-duotone btn" onClick={() => toggle()} />
)
}
function GoTop() { function GoTop() {
const { ok, fn: goToTop } = useAtomValue(goToTopAtom) const { ok, fn: goToTop } = useAtomValue(goToTopAtom)
return ( return (
@ -74,6 +81,7 @@ export function Header() {
<span className="justify-self-end flex gap-2 items-center text-xl text-primary-600 dark:text-primary"> <span className="justify-self-end flex gap-2 items-center text-xl text-primary-600 dark:text-primary">
<GoTop /> <GoTop />
<Refresh /> <Refresh />
<Search />
<Menu /> <Menu />
</span> </span>
</> </>

View File

@ -1,24 +1,35 @@
import { columnIds, metadata } from "@shared/metadata" import { fixedColumnIds, metadata } from "@shared/metadata"
import { Link } from "@tanstack/react-router" import { Link } from "@tanstack/react-router"
import clsx from "clsx" import clsx from "clsx"
import { useAtomValue } from "jotai" import { useAtomValue } from "jotai"
import { currentColumnIDAtom } from "~/atoms" import { currentColumnIDAtom } from "~/atoms"
import { useSearchBar } from "~/hooks/useSearch"
export function NavBar() { export function NavBar() {
const currentId = useAtomValue(currentColumnIDAtom) const currentId = useAtomValue(currentColumnIDAtom)
const { toggle } = useSearchBar()
return ( return (
<span className={clsx([ <span className={clsx([
"flex p-3 rounded-2xl bg-primary/1", "flex p-3 rounded-2xl bg-primary/1 text-sm",
"shadow shadow-primary/20 hover:shadow-primary/50 transition-shadow-500", "shadow shadow-primary/20 hover:shadow-primary/50 transition-shadow-500",
])} ])}
> >
{columnIds.map(columnId => ( <button
type="button"
onClick={() => toggle(true)}
className={clsx(
"px-2 hover:(bg-primary/10 rounded-md) op-70 dark:op-90",
)}
>
</button>
{fixedColumnIds.map(columnId => (
<Link <Link
key={columnId} key={columnId}
to="/c/$column" to="/c/$column"
params={{ column: columnId }} params={{ column: columnId }}
className={clsx( className={clsx(
"text-sm px-2 hover:(bg-primary/10 rounded-md)", "px-2 hover:(bg-primary/10 rounded-md)",
currentId === columnId ? "color-primary font-bold" : "op-70 dark:op-90", currentId === columnId ? "color-primary font-bold" : "op-70 dark:op-90",
)} )}
> >

30
src/hooks/useFocus.ts Normal file
View File

@ -0,0 +1,30 @@
import { useCallback, useMemo } from "react"
import { useAtom } from "jotai"
import type { SourceID } from "@shared/types"
import { focusSourcesAtom } from "~/atoms"
export function useFocus() {
const [focusSources, setFocusSources] = useAtom(focusSourcesAtom)
const toggleFocus = useCallback((id: SourceID) => {
setFocusSources(focusSources.includes(id) ? focusSources.filter(i => i !== id) : [...focusSources, id])
}, [setFocusSources, focusSources])
const isFocused = useCallback((id: SourceID) => focusSources.includes(id), [focusSources])
return {
toggleFocus,
isFocused,
}
}
export function useFocusWith(id: SourceID) {
const [focusSources, setFocusSources] = useAtom(focusSourcesAtom)
const toggleFocus = useCallback(() => {
setFocusSources(focusSources.includes(id) ? focusSources.filter(i => i !== id) : [...focusSources, id])
}, [setFocusSources, focusSources, id])
const isFocused = useMemo(() => focusSources.includes(id), [id, focusSources])
return {
toggleFocus,
isFocused,
}
}

16
src/hooks/useSearch.ts Normal file
View File

@ -0,0 +1,16 @@
import { atom, useAtom } from "jotai"
import { useCallback } from "react"
const searchBarAtom = atom(false)
export function useSearchBar() {
const [opened, setOpened] = useAtom(searchBarAtom)
const toggle = useCallback((status?: boolean) => {
if (status !== undefined) setOpened(status)
else setOpened(v => !v)
}, [setOpened])
return {
opened,
toggle,
}
}

View File

@ -1,8 +1,8 @@
import { useSetAtom } from "jotai" import { atom, useSetAtom } from "jotai"
import { useCallback } from "react" import { useCallback } from "react"
import { toastAtom } from "~/atoms"
import type { ToastItem } from "~/atoms/types" import type { ToastItem } from "~/atoms/types"
export const toastAtom = atom<ToastItem[]>([])
export function useToast() { export function useToast() {
const setToastItems = useSetAtom(toastAtom) const setToastItems = useSetAtom(toastAtom)
return useCallback((msg: string, props?: Omit<ToastItem, "id" | "msg">) => { return useCallback((msg: string, props?: Omit<ToastItem, "id" | "msg">) => {

View File

@ -12,6 +12,7 @@ import { useSync } from "~/hooks/useSync"
import { Footer } from "~/components/footer" import { Footer } from "~/components/footer"
import { Toast } from "~/components/common/toast" import { Toast } from "~/components/common/toast"
import { usePWA } from "~/hooks/usePWA" import { usePWA } from "~/hooks/usePWA"
import { SearchBar } from "~/components/common/search-bar"
export const Route = createRootRouteWithContext<{ export const Route = createRootRouteWithContext<{
queryClient: QueryClient queryClient: QueryClient
@ -73,6 +74,7 @@ function RootComponent() {
</footer> </footer>
</GlobalOverlayScrollbar> </GlobalOverlayScrollbar>
<Toast /> <Toast />
<SearchBar />
{import.meta.env.DEV && ( {import.meta.env.DEV && (
<> <>
<ReactQueryDevtools buttonPosition="bottom-left" /> <ReactQueryDevtools buttonPosition="bottom-left" />

View File

@ -1,12 +1,12 @@
import { createFileRoute, redirect } from "@tanstack/react-router" import { createFileRoute, redirect } from "@tanstack/react-router"
import { columnIds } from "@shared/metadata" import { fixedColumnIds } from "@shared/metadata"
import { Column } from "~/components/column" import { Column } from "~/components/column"
export const Route = createFileRoute("/c/$column")({ export const Route = createFileRoute("/c/$column")({
component: SectionComponent, component: SectionComponent,
params: { params: {
parse: (params) => { parse: (params) => {
const column = columnIds.find(x => x === params.column.toLowerCase()) const column = fixedColumnIds.find(x => x === params.column.toLowerCase())
if (!column) throw new Error(`"${params.column}" is not a valid column.`) if (!column) throw new Error(`"${params.column}" is not a valid column.`)
return { return {
column, column,