From c3a825de74c7eae45bf63ee7120cc29e909cc4bf Mon Sep 17 00:00:00 2001 From: Ou Date: Mon, 28 Oct 2024 21:28:13 +0800 Subject: [PATCH] feat: use command bar to search and focus source --- package.json | 4 +- pnpm-lock.yaml | 598 +++++++++++++++++++++ scripts/pinyin.ts | 13 + server/api/s/[id].ts | 3 + shared/metadata.ts | 13 +- shared/pinyin.json | 30 ++ shared/types.ts | 12 +- src/atoms/index.ts | 8 +- src/atoms/primitiveMetadataAtom.ts | 8 +- src/components/column/card.tsx | 32 +- src/components/common/search-bar/cmdk.css | 77 +++ src/components/common/search-bar/index.tsx | 142 +++++ src/components/common/toast.tsx | 2 +- src/components/header/index.tsx | 8 + src/components/navbar.tsx | 19 +- src/hooks/useFocus.ts | 30 ++ src/hooks/useSearch.ts | 16 + src/hooks/useToast.ts | 4 +- src/routes/__root.tsx | 2 + src/routes/c.$column.tsx | 4 +- 20 files changed, 982 insertions(+), 43 deletions(-) create mode 100644 scripts/pinyin.ts create mode 100644 shared/pinyin.json create mode 100644 src/components/common/search-bar/cmdk.css create mode 100644 src/components/common/search-bar/index.tsx create mode 100644 src/hooks/useFocus.ts create mode 100644 src/hooks/useSearch.ts diff --git a/package.json b/package.json index 49f395e..edcfacf 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "dev": "vite dev", "build": "vite build", "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", "preview": "CF_PAGES=1 pnpm run build && wrangler pages dev", "deploy": "CF_PAGES=1 pnpm run build && wrangler pages deploy", @@ -36,6 +36,7 @@ "better-sqlite3": "^11.3.0", "cheerio": "^1.0.0", "clsx": "^2.1.1", + "cmdk": "^1.0.0", "consola": "^3.2.3", "cookie-es": "^1.2.2", "dayjs": "1.11.13", @@ -60,6 +61,7 @@ "devDependencies": { "@eslint-react/eslint-plugin": "^1.14.3", "@iconify-json/ph": "^1.2.1", + "@napi-rs/pinyin": "^1.7.5", "@ourongxing/eslint-config": "3.2.3-beta.6", "@ourongxing/tsconfig": "^0.0.4", "@rollup/pluginutils": "^5.1.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ccea0df..fdd4cef 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,9 @@ importers: clsx: specifier: ^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: specifier: ^3.2.3 version: 3.2.3 @@ -114,6 +117,9 @@ importers: '@iconify-json/ph': specifier: ^1.2.1 version: 1.2.1 + '@napi-rs/pinyin': + specifier: ^1.7.5 + version: 1.7.5 '@ourongxing/eslint-config': 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)) @@ -1547,6 +1553,91 @@ packages: resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} 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': resolution: {integrity: sha512-DeoAQh8LuNPvBE4qsKlezjKj0PyXDryOFJfJKo3Z1qZLKzQ21sT314KQKPVjfvw6knqijj+IO+0kHXy/TJiqNA==} engines: {node: '>=14.0.0'} @@ -1713,6 +1804,168 @@ packages: '@polka/url@1.0.0-next.28': 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': resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==} @@ -2445,6 +2698,10 @@ packages: argparse@2.0.1: 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: resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} engines: {node: '>= 0.4'} @@ -2698,6 +2955,12 @@ packages: resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} 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: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} @@ -2958,6 +3221,9 @@ packages: resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} engines: {node: '>=8'} + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + doctrine@3.0.0: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} @@ -3570,6 +3836,10 @@ packages: resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} 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: resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==} @@ -3800,6 +4070,9 @@ packages: resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} engines: {node: '>= 0.4'} + invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + ioredis@5.4.1: resolution: {integrity: sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==} engines: {node: '>=12.22.0'} @@ -4735,6 +5008,36 @@ packages: peerDependencies: 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: resolution: {integrity: sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==} peerDependencies: @@ -5622,6 +5925,26 @@ packages: urlpattern-polyfill@8.0.2: 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: resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==} peerDependencies: @@ -7244,6 +7567,65 @@ snapshots: - encoding - 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': dependencies: '@netlify/serverless-functions-api': 1.26.1 @@ -7387,6 +7769,157 @@ snapshots: '@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': dependencies: fast-deep-equal: 3.1.3 @@ -8267,6 +8800,10 @@ snapshots: argparse@2.0.1: {} + aria-hidden@1.2.4: + dependencies: + tslib: 2.8.0 + array-buffer-byte-length@1.0.1: dependencies: call-bind: 1.0.7 @@ -8587,6 +9124,16 @@ snapshots: 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: dependencies: color-name: 1.1.3 @@ -8784,6 +9331,8 @@ snapshots: detect-libc@2.0.3: {} + detect-node-es@1.1.0: {} + doctrine@3.0.0: dependencies: esutils: 2.0.3 @@ -9663,6 +10212,8 @@ snapshots: has-symbols: 1.0.3 hasown: 2.0.2 + get-nonce@1.0.1: {} + get-own-enumerable-property-symbols@3.0.2: {} get-port-please@3.1.2: {} @@ -9924,6 +10475,10 @@ snapshots: hasown: 2.0.2 side-channel: 1.0.6 + invariant@2.2.4: + dependencies: + loose-envify: 1.4.0 + ioredis@5.4.1: dependencies: '@ioredis/commands': 1.2.0 @@ -10914,6 +11469,34 @@ snapshots: react: 18.3.1 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): dependencies: react: 18.3.1 @@ -11883,6 +12466,21 @@ snapshots: 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): dependencies: react: 18.3.1 diff --git a/scripts/pinyin.ts b/scripts/pinyin.ts new file mode 100644 index 0000000..d24ccb6 --- /dev/null +++ b/scripts/pinyin.ts @@ -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)) diff --git a/server/api/s/[id].ts b/server/api/s/[id].ts index 8271456..6f8eb28 100644 --- a/server/api/s/[id].ts +++ b/server/api/s/[id].ts @@ -30,6 +30,7 @@ export default defineEventHandler(async (event): Promise => { if (now - cache.updated < interval) { return { status: "success", + id, updatedTime: now, items: cache.data, } @@ -46,6 +47,7 @@ export default defineEventHandler(async (event): Promise => { if (!latest || (!event.context.disabledLogin && !event.context.user)) { return { status: "cache", + id, updatedTime: cache.updated, items: cache.data, } @@ -62,6 +64,7 @@ export default defineEventHandler(async (event): Promise => { } return { status: "success", + id, updatedTime: now, items: data, } diff --git a/shared/metadata.ts b/shared/metadata.ts index 678ad67..fb4dca8 100644 --- a/shared/metadata.ts +++ b/shared/metadata.ts @@ -1,10 +1,8 @@ import { sources } from "./sources" 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 - -const columnName: Record = { +export const columns = { china: { zh: "国内", }, @@ -26,9 +24,12 @@ const columnName: Record = { hottest: { zh: "最热", }, -} +} as const -export const metadata: Metadata = typeSafeObjectFromEntries(typeSafeObjectEntries(columnName).map(([k, v]) => { +export const fixedColumnIds = ["focus", "hottest", "realtime"] as const satisfies Partial[] +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) { case "focus": return [k, { diff --git a/shared/pinyin.json b/shared/pinyin.json new file mode 100644 index 0000000..411fb96 --- /dev/null +++ b/shared/pinyin.json @@ -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" +} diff --git a/shared/types.ts b/shared/types.ts index 6905e04..91f010a 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -1,5 +1,5 @@ import type { colors } from "unocss/preset-mini" -import type { columnIds } from "./metadata" +import type { columns, fixedColumnIds } from "./metadata" import type { originSources } from "./sources" export type Color = "primary" | Exclude @@ -24,16 +24,17 @@ export type AllSourceID = { // export type DisabledSourceID = Exclude -export type ColumnID = (typeof columnIds)[number] +export type ColumnID = keyof typeof columns export type Metadata = Record export interface PrimitiveMetadata { updatedTime: number - data: Record + data: Record action: "init" | "manual" | "sync" } -type ManualColumnID = Exclude +export type FixedColumnID = (typeof fixedColumnIds)[number] +export type HiddenColumnID = Exclude export interface OriginSource extends Partial> { name: string @@ -69,7 +70,7 @@ export interface Source { * Default normal timeline */ type?: "hottest" | "realtime" - column?: ManualColumnID + column?: HiddenColumnID home?: string /** * @default false @@ -103,6 +104,7 @@ export interface NewsItem { export interface SourceResponse { status: "success" | "cache" + id: SourceID updatedTime: number | string items: NewsItem[] } diff --git a/src/atoms/index.ts b/src/atoms/index.ts index 1096c10..7e76847 100644 --- a/src/atoms/index.ts +++ b/src/atoms/index.ts @@ -1,8 +1,8 @@ import { atom } from "jotai" -import type { ColumnID, SourceID } from "@shared/types" +import type { FixedColumnID, SourceID } from "@shared/types" import { sources } from "@shared/sources" import { primitiveMetadataAtom } from "./primitiveMetadataAtom" -import type { ToastItem, Update } from "./types" +import type { Update } from "./types" export { primitiveMetadataAtom, preprocessMetadata } from "./primitiveMetadataAtom" @@ -35,7 +35,7 @@ function initRefetchSources() { export const refetchSourcesAtom = atom(initRefetchSources()) -export const currentColumnIDAtom = atom("focus") +export const currentColumnIDAtom = atom("focus") export const currentSourcesAtom = atom((get) => { const id = get(currentColumnIDAtom) @@ -56,5 +56,3 @@ export const goToTopAtom = atom({ ok: false, fn: undefined as (() => void) | undefined, }) - -export const toastAtom = atom([]) diff --git a/src/atoms/primitiveMetadataAtom.ts b/src/atoms/primitiveMetadataAtom.ts index 61fc111..4bee0a6 100644 --- a/src/atoms/primitiveMetadataAtom.ts +++ b/src/atoms/primitiveMetadataAtom.ts @@ -1,8 +1,8 @@ -import { metadata } from "@shared/metadata" +import { fixedColumnIds, metadata } from "@shared/metadata" import { typeSafeObjectEntries, typeSafeObjectFromEntries } from "@shared/type.util" import type { PrimitiveAtom } 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 { sources } from "@shared/sources" import type { Update } from "./types" @@ -37,7 +37,9 @@ function createPrimitiveMetadataAtom( 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) { return { data: { diff --git a/src/components/column/card.tsx b/src/components/column/card.tsx index bce7a13..df20637 100644 --- a/src/components/column/card.tsx +++ b/src/components/column/card.tsx @@ -9,9 +9,10 @@ import type { SyntheticListenerMap } from "@dnd-kit/core/dist/hooks/utilities" import { ofetch } from "ofetch" import { useWindowSize } from "react-use" import { OverlayScrollbar } from "../common/overlay-scrollbar" -import { focusSourcesAtom, refetchSourcesAtom } from "~/atoms" +import { refetchSourcesAtom } from "~/atoms" import { useRelativeTime } from "~/hooks/useRelativeTime" import { safeParseString } from "~/utils" +import { useFocusWith } from "~/hooks/useFocus" export interface ItemsProps extends React.HTMLAttributes { id: SourceID @@ -56,7 +57,6 @@ export const CardWrapper = forwardRef(({ id, isDragg const prevSourceItems: Partial> = {} function NewsCard({ id, inView, handleListeners }: NewsCardProps) { - const [focusSources, setFocusSources] = useAtom(focusSourcesAtom) const [refetchSource, setRefetchSource] = useAtom(refetchSourcesAtom) const { data, isFetching, isPlaceholderData, isError } = useQuery({ queryKey: [id, refetchSource[id]], @@ -92,16 +92,15 @@ function NewsCard({ id, inView, handleListeners }: NewsCardProps) { }, // refetch 时显示原有的数据 placeholderData: (prev) => { - if (prev?.items && sources[id].type === "hottest") prevSourceItems[id] = prev.items - return prev + if (prev?.id === id) { + if (prev?.items && sources[id].type === "hottest") prevSourceItems[id] = prev.items + return prev + } }, staleTime: 1000 * 60 * 5, enabled: inView, }) - const addFocusList = useCallback(() => { - setFocusSources(focusSources.includes(id) ? focusSources.filter(i => i !== id) : [...focusSources, id]) - }, [setFocusSources, focusSources, id]) const manualRefetch = useCallback(() => { setRefetchSource(prev => ({ ...prev, @@ -111,12 +110,15 @@ function NewsCard({ id, inView, handleListeners }: NewsCardProps) { const isFreshFetching = useMemo(() => isFetching && !isPlaceholderData, [isFetching, isPlaceholderData]) + const { isFocused, toggleFocus } = useFocusWith(id) + return ( <> diff --git a/src/components/common/search-bar/cmdk.css b/src/components/common/search-bar/cmdk.css new file mode 100644 index 0000000..4a49bd3 --- /dev/null +++ b/src/components/common/search-bar/cmdk.css @@ -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; +} \ No newline at end of file diff --git a/src/components/common/search-bar/index.tsx b/src/components/common/search-bar/index.tsx new file mode 100644 index 0000000..4940583 --- /dev/null +++ b/src/components/common/search-bar/index.tsx @@ -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(null) + + const [value, setValue] = useState("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 ( + { + if (v in sources) { + setValue(v as SourceID) + } + }} + > + +
+ + + 没有找到,可以前往 Github 提 issue + { + sourceItems.map(({ column, sources }) => ( + + { + sources.map(item => ) + } + + ), + ) + } + + +
+ +
+
+
+ ) +} + +function SourceItem({ item }: { + item: SourceItemProps +}) { + const { isFocused, toggleFocus } = useFocusWith(item.id) + return ( + + + + {item.name} + {item.title} + + + + ) +} diff --git a/src/components/common/toast.tsx b/src/components/common/toast.tsx index b99d3cd..ac97ca1 100644 --- a/src/components/common/toast.tsx +++ b/src/components/common/toast.tsx @@ -3,8 +3,8 @@ import { AnimatePresence, motion } from "framer-motion" import { useAtomValue, useSetAtom } from "jotai" import { useCallback, useMemo, useRef } from "react" import { useHoverDirty, useMount, useUpdateEffect, useWindowSize } from "react-use" -import { toastAtom } from "~/atoms" import type { ToastItem } from "~/atoms/types" +import { toastAtom } from "~/hooks/useToast" import { Timer } from "~/utils" const WIDTH = 320 diff --git a/src/components/header/index.tsx b/src/components/header/index.tsx index ebd8346..88bed6f 100644 --- a/src/components/header/index.tsx +++ b/src/components/header/index.tsx @@ -8,7 +8,14 @@ import { Homepage, Version } from "@shared/consts" import { NavBar } from "../navbar" import { Menu } from "./menu" import { currentSourcesAtom, goToTopAtom, refetchSourcesAtom } from "~/atoms" +import { useSearchBar } from "~/hooks/useSearch" +export function Search() { + const { toggle } = useSearchBar() + return ( + + {fixedColumnIds.map(columnId => ( diff --git a/src/hooks/useFocus.ts b/src/hooks/useFocus.ts new file mode 100644 index 0000000..48b7577 --- /dev/null +++ b/src/hooks/useFocus.ts @@ -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, + } +} diff --git a/src/hooks/useSearch.ts b/src/hooks/useSearch.ts new file mode 100644 index 0000000..cf5f151 --- /dev/null +++ b/src/hooks/useSearch.ts @@ -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, + } +} diff --git a/src/hooks/useToast.ts b/src/hooks/useToast.ts index 195f9c2..1694bbf 100644 --- a/src/hooks/useToast.ts +++ b/src/hooks/useToast.ts @@ -1,8 +1,8 @@ -import { useSetAtom } from "jotai" +import { atom, useSetAtom } from "jotai" import { useCallback } from "react" -import { toastAtom } from "~/atoms" import type { ToastItem } from "~/atoms/types" +export const toastAtom = atom([]) export function useToast() { const setToastItems = useSetAtom(toastAtom) return useCallback((msg: string, props?: Omit) => { diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 43cbdbe..7261a35 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -12,6 +12,7 @@ import { useSync } from "~/hooks/useSync" import { Footer } from "~/components/footer" import { Toast } from "~/components/common/toast" import { usePWA } from "~/hooks/usePWA" +import { SearchBar } from "~/components/common/search-bar" export const Route = createRootRouteWithContext<{ queryClient: QueryClient @@ -73,6 +74,7 @@ function RootComponent() { + {import.meta.env.DEV && ( <> diff --git a/src/routes/c.$column.tsx b/src/routes/c.$column.tsx index 0749efd..237ed4f 100644 --- a/src/routes/c.$column.tsx +++ b/src/routes/c.$column.tsx @@ -1,12 +1,12 @@ import { createFileRoute, redirect } from "@tanstack/react-router" -import { columnIds } from "@shared/metadata" +import { fixedColumnIds } from "@shared/metadata" import { Column } from "~/components/column" export const Route = createFileRoute("/c/$column")({ component: SectionComponent, 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.`) return { column,