feat: add source icon
@ -9,6 +9,7 @@
|
|||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"nitro": "nitro build",
|
"nitro": "nitro build",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
|
"favicon": "tsx ./scripts/favicon.ts",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
@ -21,12 +22,11 @@
|
|||||||
"@tanstack/react-router": "^1.58.9",
|
"@tanstack/react-router": "^1.58.9",
|
||||||
"@tanstack/router-devtools": "^1.58.9",
|
"@tanstack/router-devtools": "^1.58.9",
|
||||||
"@unocss/reset": "^0.62.4",
|
"@unocss/reset": "^0.62.4",
|
||||||
"array-differences": "^3.0.2",
|
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dayjs": "^1.11.13",
|
"favicons-scraper": "^1.3.2",
|
||||||
"framer-motion": "^11.8.0",
|
|
||||||
"h3": "^1.12.0",
|
"h3": "^1.12.0",
|
||||||
"jotai": "^2.10.0",
|
"jotai": "^2.10.0",
|
||||||
|
"node-fetch": "^3.3.2",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-intersection-observer": "^9.13.1",
|
"react-intersection-observer": "^9.13.1",
|
||||||
@ -45,8 +45,8 @@
|
|||||||
"eslint": "^9.11.1",
|
"eslint": "^9.11.1",
|
||||||
"eslint-plugin-react-hooks": "^5.1.0-rc-778e1ed2-20240926",
|
"eslint-plugin-react-hooks": "^5.1.0-rc-778e1ed2-20240926",
|
||||||
"eslint-plugin-react-refresh": "^0.4.12",
|
"eslint-plugin-react-refresh": "^0.4.12",
|
||||||
"globals": "^15.9.0",
|
|
||||||
"nitropack": "^2.9.7",
|
"nitropack": "^2.9.7",
|
||||||
|
"tsx": "^4.19.1",
|
||||||
"typescript": "^5.6.2",
|
"typescript": "^5.6.2",
|
||||||
"typescript-eslint": "^8.7.0",
|
"typescript-eslint": "^8.7.0",
|
||||||
"unocss": "^0.62.4",
|
"unocss": "^0.62.4",
|
||||||
|
92
pnpm-lock.yaml
generated
@ -32,24 +32,21 @@ importers:
|
|||||||
'@unocss/reset':
|
'@unocss/reset':
|
||||||
specifier: ^0.62.4
|
specifier: ^0.62.4
|
||||||
version: 0.62.4
|
version: 0.62.4
|
||||||
array-differences:
|
|
||||||
specifier: ^3.0.2
|
|
||||||
version: 3.0.2
|
|
||||||
clsx:
|
clsx:
|
||||||
specifier: ^2.1.1
|
specifier: ^2.1.1
|
||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
dayjs:
|
favicons-scraper:
|
||||||
specifier: ^1.11.13
|
specifier: ^1.3.2
|
||||||
version: 1.11.13
|
version: 1.3.2
|
||||||
framer-motion:
|
|
||||||
specifier: ^11.8.0
|
|
||||||
version: 11.8.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
|
||||||
h3:
|
h3:
|
||||||
specifier: ^1.12.0
|
specifier: ^1.12.0
|
||||||
version: 1.12.0
|
version: 1.12.0
|
||||||
jotai:
|
jotai:
|
||||||
specifier: ^2.10.0
|
specifier: ^2.10.0
|
||||||
version: 2.10.0(@types/react@18.3.9)(react@18.3.1)
|
version: 2.10.0(@types/react@18.3.9)(react@18.3.1)
|
||||||
|
node-fetch:
|
||||||
|
specifier: ^3.3.2
|
||||||
|
version: 3.3.2
|
||||||
react:
|
react:
|
||||||
specifier: ^18.3.1
|
specifier: ^18.3.1
|
||||||
version: 18.3.1
|
version: 18.3.1
|
||||||
@ -99,12 +96,12 @@ importers:
|
|||||||
eslint-plugin-react-refresh:
|
eslint-plugin-react-refresh:
|
||||||
specifier: ^0.4.12
|
specifier: ^0.4.12
|
||||||
version: 0.4.12(eslint@9.11.1(jiti@1.21.6))
|
version: 0.4.12(eslint@9.11.1(jiti@1.21.6))
|
||||||
globals:
|
|
||||||
specifier: ^15.9.0
|
|
||||||
version: 15.9.0
|
|
||||||
nitropack:
|
nitropack:
|
||||||
specifier: ^2.9.7
|
specifier: ^2.9.7
|
||||||
version: 2.9.7(magicast@0.3.5)
|
version: 2.9.7(magicast@0.3.5)
|
||||||
|
tsx:
|
||||||
|
specifier: ^4.19.1
|
||||||
|
version: 4.19.1
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.6.2
|
specifier: ^5.6.2
|
||||||
version: 5.6.2
|
version: 5.6.2
|
||||||
@ -1799,9 +1796,6 @@ 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==}
|
||||||
|
|
||||||
array-differences@3.0.2:
|
|
||||||
resolution: {integrity: sha512-raP8rW7DyS3AypmGHlvuSxXUdTXVvQxPHDlWFyQ0DLw4BMb//1qJtUlBRRK6ePLfQV3B3L5tLSe7qix+uEf14w==}
|
|
||||||
|
|
||||||
assertion-error@2.0.1:
|
assertion-error@2.0.1:
|
||||||
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@ -2064,8 +2058,9 @@ packages:
|
|||||||
csstype@3.1.3:
|
csstype@3.1.3:
|
||||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||||
|
|
||||||
dayjs@1.11.13:
|
data-uri-to-buffer@4.0.1:
|
||||||
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
|
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
|
||||||
|
engines: {node: '>= 12'}
|
||||||
|
|
||||||
db0@0.1.4:
|
db0@0.1.4:
|
||||||
resolution: {integrity: sha512-Ft6eCwONYxlwLjBXSJxw0t0RYtA5gW9mq8JfBXn9TtC0nDPlqePAhpv9v4g9aONBi6JI1OXHTKKkUYGd+BOrCA==}
|
resolution: {integrity: sha512-Ft6eCwONYxlwLjBXSJxw0t0RYtA5gW9mq8JfBXn9TtC0nDPlqePAhpv9v4g9aONBi6JI1OXHTKKkUYGd+BOrCA==}
|
||||||
@ -2554,6 +2549,9 @@ packages:
|
|||||||
fastq@1.17.1:
|
fastq@1.17.1:
|
||||||
resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
|
resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
|
||||||
|
|
||||||
|
favicons-scraper@1.3.2:
|
||||||
|
resolution: {integrity: sha512-rsEBzLyEdJFEg/fzAx0vl8HypWwjSxisdHTaZ1W/jrKRuCm+0GG4xVWrOfhvG18dAhG2eKBW0tfhGarQt/HL8w==}
|
||||||
|
|
||||||
fdir@6.3.0:
|
fdir@6.3.0:
|
||||||
resolution: {integrity: sha512-QOnuT+BOtivR77wYvCWHfGt9s4Pz1VIMbD463vegT5MLqNXy8rYFT/lPVEqf/bhYeT6qmqrNHhsX+rWwe3rOCQ==}
|
resolution: {integrity: sha512-QOnuT+BOtivR77wYvCWHfGt9s4Pz1VIMbD463vegT5MLqNXy8rYFT/lPVEqf/bhYeT6qmqrNHhsX+rWwe3rOCQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -2562,6 +2560,10 @@ packages:
|
|||||||
picomatch:
|
picomatch:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
fetch-blob@3.2.0:
|
||||||
|
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
|
||||||
|
engines: {node: ^12.20 || >= 14.13}
|
||||||
|
|
||||||
file-entry-cache@8.0.0:
|
file-entry-cache@8.0.0:
|
||||||
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
|
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
|
||||||
engines: {node: '>=16.0.0'}
|
engines: {node: '>=16.0.0'}
|
||||||
@ -2596,19 +2598,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==}
|
resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
framer-motion@11.8.0:
|
formdata-polyfill@4.0.10:
|
||||||
resolution: {integrity: sha512-q/axN/PFRdKmzPK6PO2OhbLUMWXXZuiejdM1/3FhC2hm4YIetc+qeco2EvWm4u1/UTFmevclE492wGFNfSZ4eQ==}
|
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
|
||||||
peerDependencies:
|
engines: {node: '>=12.20.0'}
|
||||||
'@emotion/is-prop-valid': '*'
|
|
||||||
react: ^18.0.0
|
|
||||||
react-dom: ^18.0.0
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@emotion/is-prop-valid':
|
|
||||||
optional: true
|
|
||||||
react:
|
|
||||||
optional: true
|
|
||||||
react-dom:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
fresh@0.5.2:
|
fresh@0.5.2:
|
||||||
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
|
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
|
||||||
@ -3207,6 +3199,10 @@ packages:
|
|||||||
node-addon-api@7.1.1:
|
node-addon-api@7.1.1:
|
||||||
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
|
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
|
||||||
|
|
||||||
|
node-domexception@1.0.0:
|
||||||
|
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||||
|
engines: {node: '>=10.5.0'}
|
||||||
|
|
||||||
node-fetch-native@1.6.4:
|
node-fetch-native@1.6.4:
|
||||||
resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==}
|
resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==}
|
||||||
|
|
||||||
@ -3219,6 +3215,10 @@ packages:
|
|||||||
encoding:
|
encoding:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
node-fetch@3.3.2:
|
||||||
|
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
|
||||||
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
|
|
||||||
node-forge@1.3.1:
|
node-forge@1.3.1:
|
||||||
resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==}
|
resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==}
|
||||||
engines: {node: '>= 6.13.0'}
|
engines: {node: '>= 6.13.0'}
|
||||||
@ -4180,6 +4180,10 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: '>=6.0.0'
|
eslint: '>=6.0.0'
|
||||||
|
|
||||||
|
web-streams-polyfill@3.3.3:
|
||||||
|
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
|
||||||
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
webidl-conversions@3.0.1:
|
webidl-conversions@3.0.1:
|
||||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||||
|
|
||||||
@ -5859,8 +5863,6 @@ snapshots:
|
|||||||
|
|
||||||
argparse@2.0.1: {}
|
argparse@2.0.1: {}
|
||||||
|
|
||||||
array-differences@3.0.2: {}
|
|
||||||
|
|
||||||
assertion-error@2.0.1: {}
|
assertion-error@2.0.1: {}
|
||||||
|
|
||||||
async-sema@3.1.1: {}
|
async-sema@3.1.1: {}
|
||||||
@ -6110,7 +6112,7 @@ snapshots:
|
|||||||
|
|
||||||
csstype@3.1.3: {}
|
csstype@3.1.3: {}
|
||||||
|
|
||||||
dayjs@1.11.13: {}
|
data-uri-to-buffer@4.0.1: {}
|
||||||
|
|
||||||
db0@0.1.4: {}
|
db0@0.1.4: {}
|
||||||
|
|
||||||
@ -6769,10 +6771,17 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
reusify: 1.0.4
|
reusify: 1.0.4
|
||||||
|
|
||||||
|
favicons-scraper@1.3.2: {}
|
||||||
|
|
||||||
fdir@6.3.0(picomatch@4.0.2):
|
fdir@6.3.0(picomatch@4.0.2):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
picomatch: 4.0.2
|
picomatch: 4.0.2
|
||||||
|
|
||||||
|
fetch-blob@3.2.0:
|
||||||
|
dependencies:
|
||||||
|
node-domexception: 1.0.0
|
||||||
|
web-streams-polyfill: 3.3.3
|
||||||
|
|
||||||
file-entry-cache@8.0.0:
|
file-entry-cache@8.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
flat-cache: 4.0.1
|
flat-cache: 4.0.1
|
||||||
@ -6807,12 +6816,9 @@ snapshots:
|
|||||||
cross-spawn: 7.0.3
|
cross-spawn: 7.0.3
|
||||||
signal-exit: 4.1.0
|
signal-exit: 4.1.0
|
||||||
|
|
||||||
framer-motion@11.8.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
formdata-polyfill@4.0.10:
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.7.0
|
fetch-blob: 3.2.0
|
||||||
optionalDependencies:
|
|
||||||
react: 18.3.1
|
|
||||||
react-dom: 18.3.1(react@18.3.1)
|
|
||||||
|
|
||||||
fresh@0.5.2: {}
|
fresh@0.5.2: {}
|
||||||
|
|
||||||
@ -7486,12 +7492,20 @@ snapshots:
|
|||||||
|
|
||||||
node-addon-api@7.1.1: {}
|
node-addon-api@7.1.1: {}
|
||||||
|
|
||||||
|
node-domexception@1.0.0: {}
|
||||||
|
|
||||||
node-fetch-native@1.6.4: {}
|
node-fetch-native@1.6.4: {}
|
||||||
|
|
||||||
node-fetch@2.7.0:
|
node-fetch@2.7.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
whatwg-url: 5.0.0
|
whatwg-url: 5.0.0
|
||||||
|
|
||||||
|
node-fetch@3.3.2:
|
||||||
|
dependencies:
|
||||||
|
data-uri-to-buffer: 4.0.1
|
||||||
|
fetch-blob: 3.2.0
|
||||||
|
formdata-polyfill: 4.0.10
|
||||||
|
|
||||||
node-forge@1.3.1: {}
|
node-forge@1.3.1: {}
|
||||||
|
|
||||||
node-gyp-build@4.8.2: {}
|
node-gyp-build@4.8.2: {}
|
||||||
@ -8496,6 +8510,8 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
web-streams-polyfill@3.3.3: {}
|
||||||
|
|
||||||
webidl-conversions@3.0.1: {}
|
webidl-conversions@3.0.1: {}
|
||||||
|
|
||||||
webpack-virtual-modules@0.6.2: {}
|
webpack-virtual-modules@0.6.2: {}
|
||||||
|
BIN
public/icons/36kr.png
Normal file
After Width: | Height: | Size: 879 B |
BIN
public/icons/acfun.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
public/icons/baidu.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
public/icons/bilibili.png
Normal file
After Width: | Height: | Size: 5.5 KiB |
BIN
public/icons/cankaoxiaoxi.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
public/icons/douyin.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
public/icons/genshin.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
public/icons/github.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
public/icons/hellogithub.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
public/icons/honkai.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
public/icons/hupu.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
public/icons/ithome.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
public/icons/jianshu.png
Normal file
After Width: | Height: | Size: 6.7 KiB |
BIN
public/icons/juejin.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
BIN
public/icons/kuaishou.png
Normal file
After Width: | Height: | Size: 6.8 KiB |
BIN
public/icons/lol.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
public/icons/peopledaily.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
public/icons/sspai.png
Normal file
After Width: | Height: | Size: 8.7 KiB |
BIN
public/icons/starrail.png
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
public/icons/thepaper.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
public/icons/tieba.png
Normal file
After Width: | Height: | Size: 5.6 KiB |
BIN
public/icons/toutiao.png
Normal file
After Width: | Height: | Size: 7.7 KiB |
BIN
public/icons/v2ex.png
Normal file
After Width: | Height: | Size: 41 KiB |
BIN
public/icons/weibo.png
Normal file
After Width: | Height: | Size: 9.9 KiB |
BIN
public/icons/weread.png
Normal file
After Width: | Height: | Size: 4.9 KiB |
BIN
public/icons/zaobao.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
public/icons/zhihu.png
Normal file
After Width: | Height: | Size: 6.9 KiB |
49
scripts/favicon.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import fs from "node:fs"
|
||||||
|
import { fileURLToPath } from "node:url"
|
||||||
|
import { join } from "node:path"
|
||||||
|
import { getLogos } from "favicons-scraper"
|
||||||
|
import fetch from "node-fetch"
|
||||||
|
import { sources } from "../shared/data"
|
||||||
|
|
||||||
|
const projectDir = fileURLToPath(new URL("..", import.meta.url))
|
||||||
|
const iconsDir = join(projectDir, "public", "icons")
|
||||||
|
async function downloadImage(url: string, outputPath: string) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url)
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Could not fetch ${url}, status: ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileStream = fs.createWriteStream(outputPath)
|
||||||
|
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
response.body?.pipe(fileStream)
|
||||||
|
fileStream.on("finish", resolve)
|
||||||
|
fileStream.on("error", reject)
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log("Image downloaded successfully.")
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error downloading the image:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
await Promise.all(
|
||||||
|
Object.entries(sources).map(async ([id, source]) => {
|
||||||
|
try {
|
||||||
|
const icon = join(iconsDir, `${id}.png`)
|
||||||
|
if (fs.existsSync(icon)) return
|
||||||
|
if (!source.home) return
|
||||||
|
const res = await getLogos(source.home)
|
||||||
|
if (res.length) {
|
||||||
|
await downloadImage(res[0].src, icon)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(id, "\n", e )
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
0
server/sources/index.ts
Normal file
0
server/sources/weibo.ts
Normal file
@ -2,19 +2,55 @@ import type { Metadata } from "./types"
|
|||||||
|
|
||||||
export const sectionIds = ["focus", "social", "china", "world", "digital"] as const
|
export const sectionIds = ["focus", "social", "china", "world", "digital"] as const
|
||||||
|
|
||||||
export const sourceList = {
|
export const sources = {
|
||||||
"36kr": "36氪",
|
"36kr": {
|
||||||
"douyin": "抖音",
|
name: "36氪",
|
||||||
"hupu": "虎扑",
|
home: "https://36kr.com",
|
||||||
"zhihu": "知乎",
|
},
|
||||||
"weibo": "微博",
|
"douyin": {
|
||||||
"tieba": "贴吧",
|
name: "抖音",
|
||||||
"zaobao": "联合早报",
|
home: "https://www.douyin.com",
|
||||||
"thepaper": "澎湃新闻",
|
},
|
||||||
"toutiao": "今日头条",
|
"hupu": {
|
||||||
"cankaoxiaoxi": "参考消息",
|
name: "虎扑",
|
||||||
"peopledaily": "人民日报",
|
home: "https://hupu.com",
|
||||||
} as const satisfies Record<string, string | false>
|
},
|
||||||
|
"zhihu": {
|
||||||
|
name: "知乎",
|
||||||
|
home: "https://www.zhihu.com",
|
||||||
|
},
|
||||||
|
"weibo": {
|
||||||
|
name: "微博",
|
||||||
|
home: "https://weibo.com",
|
||||||
|
},
|
||||||
|
"tieba": {
|
||||||
|
name: "百度贴吧",
|
||||||
|
home: "https://tieba.baidu.com",
|
||||||
|
},
|
||||||
|
"zaobao": {
|
||||||
|
name: "联合早报",
|
||||||
|
home: "https://www.zaobao.com",
|
||||||
|
},
|
||||||
|
"thepaper": {
|
||||||
|
name: "澎湃新闻",
|
||||||
|
home: "https://www.thepaper.cn",
|
||||||
|
},
|
||||||
|
"toutiao": {
|
||||||
|
name: "今日头条",
|
||||||
|
home: "https://www.toutiao.com",
|
||||||
|
},
|
||||||
|
"cankaoxiaoxi": {
|
||||||
|
name: "参考消息",
|
||||||
|
home: "http://www.cankaoxiaoxi.com",
|
||||||
|
},
|
||||||
|
"peopledaily": {
|
||||||
|
name: "人民日报",
|
||||||
|
home: "http://paper.people.com.cn",
|
||||||
|
},
|
||||||
|
} as const satisfies Record<string, {
|
||||||
|
name: string
|
||||||
|
home: string
|
||||||
|
}>
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
focus: {
|
focus: {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import type { sectionIds, sourceList } from "./data"
|
import type { sectionIds, sources } from "./data"
|
||||||
|
|
||||||
export type SourceID = keyof(typeof sourceList)
|
export type SourceID = keyof(typeof sources)
|
||||||
export type SectionID = (typeof sectionIds)[number]
|
export type SectionID = (typeof sectionIds)[number]
|
||||||
export type Metadata = Record<SectionID, Section>
|
export type Metadata = Record<SectionID, Section>
|
||||||
|
|
||||||
@ -29,6 +29,5 @@ export interface SourceInfo {
|
|||||||
total: number
|
total: number
|
||||||
link?: string
|
link?: string
|
||||||
updateTime: string
|
updateTime: string
|
||||||
fromCache: boolean
|
|
||||||
data: NewsItem[]
|
data: NewsItem[]
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { atom } from "jotai"
|
import { atom } from "jotai"
|
||||||
import type { SectionID, SourceID } from "@shared/types"
|
import type { SectionID, SourceID } from "@shared/types"
|
||||||
import { metadata, sourceList } from "@shared/data"
|
import { metadata, sources } from "@shared/data"
|
||||||
import { atomWithLocalStorage } from "./hooks/atomWithLocalStorage"
|
import { atomWithLocalStorage } from "./hooks/atomWithLocalStorage"
|
||||||
|
|
||||||
export const focusSourcesAtom = atomWithLocalStorage<SourceID[]>("focusSources", [], (stored) => {
|
export const focusSourcesAtom = atomWithLocalStorage<SourceID[]>("focusSources", [], (stored) => {
|
||||||
return stored.filter(item => item in sourceList)
|
return stored.filter(item => item in sources)
|
||||||
})
|
})
|
||||||
|
|
||||||
function initRefetchSource() {
|
function initRefetchSource() {
|
||||||
@ -17,7 +17,7 @@ function initRefetchSource() {
|
|||||||
if (!Number.isNaN(quitTime) && now - quitTime < 1000) {
|
if (!Number.isNaN(quitTime) && now - quitTime < 1000) {
|
||||||
time = now
|
time = now
|
||||||
}
|
}
|
||||||
return Object.fromEntries(Object.keys(sourceList).map(k => [k, time])) as Record<SourceID, number>
|
return Object.fromEntries(Object.keys(sources).map(k => [k, time])) as Record<SourceID, number>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const refetchSourceAtom = atom(initRefetchSource())
|
export const refetchSourceAtom = atom(initRefetchSource())
|
||||||
|
@ -6,8 +6,8 @@ import clsx from "clsx"
|
|||||||
import { CSS } from "@dnd-kit/utilities"
|
import { CSS } from "@dnd-kit/utilities"
|
||||||
import { useInView } from "react-intersection-observer"
|
import { useInView } from "react-intersection-observer"
|
||||||
import { useAtom } from "jotai"
|
import { useAtom } from "jotai"
|
||||||
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef } from "react"
|
import { forwardRef, useCallback, useImperativeHandle, useRef } from "react"
|
||||||
import { sourceList } from "@shared/data"
|
import { sources } from "@shared/data"
|
||||||
import type { SyntheticListenerMap } from "@dnd-kit/core/dist/hooks/utilities"
|
import type { SyntheticListenerMap } from "@dnd-kit/core/dist/hooks/utilities"
|
||||||
import { useSortable } from "@dnd-kit/sortable"
|
import { useSortable } from "@dnd-kit/sortable"
|
||||||
import { focusSourcesAtom, refetchSourceAtom } from "~/atoms"
|
import { focusSourcesAtom, refetchSourceAtom } from "~/atoms"
|
||||||
@ -25,22 +25,13 @@ export const CardWrapper = forwardRef<HTMLDivElement, ItemsProps>(({ id, withOpa
|
|||||||
threshold: 0,
|
threshold: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
// const bindRef = useCallback((node: HTMLDivElement) => {
|
|
||||||
// inViewRef(node)
|
|
||||||
// if (typeof dndRef === "function") {
|
|
||||||
// dndRef(node)
|
|
||||||
// } else if (dndRef) {
|
|
||||||
// dndRef.current = node
|
|
||||||
// }
|
|
||||||
// }, [inViewRef, dndRef])
|
|
||||||
|
|
||||||
useImperativeHandle(dndRef, () => ref.current as HTMLDivElement)
|
useImperativeHandle(dndRef, () => ref.current as HTMLDivElement)
|
||||||
useImperativeHandle(inViewRef, () => ref.current as HTMLDivElement)
|
useImperativeHandle(inViewRef, () => ref.current as HTMLDivElement)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={clsx("flex flex-col border rounded-md px-2 h-500px", withOpacity && "op-50", isDragging ? "scale-105" : "")}
|
className={clsx("flex flex-col bg-base border rounded-md px-2 h-500px", withOpacity && "op-50", isDragging ? "" : "")}
|
||||||
key={id}
|
key={id}
|
||||||
style={{
|
style={{
|
||||||
transformOrigin: "50% 50%",
|
transformOrigin: "50% 50%",
|
||||||
@ -95,7 +86,7 @@ interface Query {
|
|||||||
|
|
||||||
function SubTitle({ query }: Query) {
|
function SubTitle({ query }: Query) {
|
||||||
const subTitle = query.data?.type
|
const subTitle = query.data?.type
|
||||||
if (subTitle) return <span>{subTitle}</span>
|
if (subTitle) return <span className="text-xs">{subTitle}</span>
|
||||||
}
|
}
|
||||||
|
|
||||||
function UpdateTime({ query }: Query) {
|
function UpdateTime({ query }: Query) {
|
||||||
@ -108,7 +99,7 @@ function UpdateTime({ query }: Query) {
|
|||||||
function Num({ num }: { num: number }) {
|
function Num({ num }: { num: number }) {
|
||||||
const color = ["bg-red-900", "bg-red-500", "bg-red-400"]
|
const color = ["bg-red-900", "bg-red-500", "bg-red-400"]
|
||||||
return (
|
return (
|
||||||
<span className={clsx("bg-active min-w-6 flex justify-center items-center rounded-md", color[num - 1])}>
|
<span className={clsx("bg-active min-w-6 flex justify-center items-center rounded-md", false && color[num - 1])}>
|
||||||
{num}
|
{num}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
@ -122,12 +113,12 @@ function NewsList({ query }: Query) {
|
|||||||
{items.slice(0, 20).map((item, i) => (
|
{items.slice(0, 20).map((item, i) => (
|
||||||
<div key={item.title} className="flex gap-2 items-center">
|
<div key={item.title} className="flex gap-2 items-center">
|
||||||
<Num num={i + 1} />
|
<Num num={i + 1} />
|
||||||
<a href={item.url} target="_blank" className="my-1 flex flex-wrap w-full justify-between items-end flex-wrap-reverse">
|
<a href={item.url} target="_blank" className="my-1 w-full flex items-center justify-between flex-wrap">
|
||||||
<span>
|
<span className="flex-1 mr-2">
|
||||||
{item.title}
|
{item.title}
|
||||||
</span>
|
</span>
|
||||||
{item.timestamp && (
|
{item.timestamp && (
|
||||||
<span className="text-xs">
|
<span className="text-xs text-gray-4/80">
|
||||||
{relativeTime(item.timestamp)}
|
{relativeTime(item.timestamp)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@ -182,10 +173,13 @@ export function NewsCard({ id, inView, isDragging, listeners }: NewsCardProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div {...listeners} className={clsx("flex justify-between py-2", isDragging ? "cursor-grabbing" : "cursor-grab")}>
|
<div {...listeners} className={clsx("flex justify-between py-2 items-center", listeners && (isDragging ? "cursor-grabbing" : "cursor-grab"))}>
|
||||||
<span className="text-lg font-bold">
|
<div className="flex items-center gap-2">
|
||||||
{sourceList[id]}
|
<img src={`/icons/${id}.png`} className="w-4 h-4 rounded" alt={id} onError={e => e.currentTarget.hidden = true} />
|
||||||
|
<span className="text-md font-bold">
|
||||||
|
{sources[id].name}
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
<SubTitle query={query} />
|
<SubTitle query={query} />
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-auto h-full">
|
<div className="overflow-auto h-full">
|
||||||
|
@ -1,75 +0,0 @@
|
|||||||
import type { CSSProperties, HTMLAttributes } from "react"
|
|
||||||
import { useSortable } from "@dnd-kit/sortable"
|
|
||||||
import { CSS } from "@dnd-kit/utilities"
|
|
||||||
import { forwardRef } from "react"
|
|
||||||
import clsx from "clsx"
|
|
||||||
import type { SyntheticListenerMap } from "@dnd-kit/core/dist/hooks/utilities"
|
|
||||||
|
|
||||||
type ItemProps = HTMLAttributes<HTMLDivElement> & {
|
|
||||||
id: string
|
|
||||||
withOpacity?: boolean
|
|
||||||
isDragging?: boolean
|
|
||||||
listeners?: SyntheticListenerMap
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Item = forwardRef<HTMLDivElement, ItemProps>(({ id, withOpacity, isDragging, listeners, style, ...props }, ref) => {
|
|
||||||
const inlineStyles: CSSProperties = {
|
|
||||||
transformOrigin: "50% 50%",
|
|
||||||
minHeight: "500px",
|
|
||||||
...style,
|
|
||||||
}
|
|
||||||
|
|
||||||
const css = [
|
|
||||||
"border rounded-xl",
|
|
||||||
isDragging ? "scale-105" : "",
|
|
||||||
withOpacity && "op-50",
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className={clsx(css)}
|
|
||||||
style={inlineStyles}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={clsx("border-b", isDragging ? "cursor-grabbing" : "cursor-grab")}
|
|
||||||
{...listeners}
|
|
||||||
>
|
|
||||||
你好
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{id}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
export function SortableItem(props: ItemProps) {
|
|
||||||
const { id } = props
|
|
||||||
const {
|
|
||||||
isDragging,
|
|
||||||
attributes,
|
|
||||||
listeners,
|
|
||||||
setNodeRef,
|
|
||||||
transform,
|
|
||||||
transition,
|
|
||||||
} = useSortable({ id })
|
|
||||||
|
|
||||||
const style = {
|
|
||||||
transform: CSS.Transform.toString(transform),
|
|
||||||
transition: transition || undefined,
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Item
|
|
||||||
ref={setNodeRef}
|
|
||||||
style={style}
|
|
||||||
withOpacity={isDragging}
|
|
||||||
isDragging={isDragging}
|
|
||||||
listeners={listeners}
|
|
||||||
{...attributes}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
@ -6,7 +6,7 @@ import { currentSectionAtom } from "~/atoms"
|
|||||||
export function GridContainer({ children }: PropsWithChildren) {
|
export function GridContainer({ children }: PropsWithChildren) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="grid w-full gap-5 mt-10"
|
className="grid w-full gap-5"
|
||||||
style={{
|
style={{
|
||||||
gridTemplateColumns: "repeat(auto-fill, minmax(350px, 1fr))",
|
gridTemplateColumns: "repeat(auto-fill, minmax(350px, 1fr))",
|
||||||
}}
|
}}
|
||||||
|
@ -24,7 +24,7 @@ export const Route = createRootRouteWithContext<{
|
|||||||
export function RootComponent() {
|
export function RootComponent() {
|
||||||
useOnReload()
|
useOnReload()
|
||||||
return (
|
return (
|
||||||
<div className="p-10">
|
<div className="md:p-10 p-4">
|
||||||
<Header />
|
<Header />
|
||||||
<Outlet />
|
<Outlet />
|
||||||
<ReactQueryDevtools buttonPosition="bottom-left" />
|
<ReactQueryDevtools buttonPosition="bottom-left" />
|
||||||
|
@ -24,7 +24,7 @@ function IndexComponent() {
|
|||||||
|
|
||||||
return id && (
|
return id && (
|
||||||
<div className="flex flex-col justify-center items-center">
|
<div className="flex flex-col justify-center items-center">
|
||||||
<section className="flex gap-2">
|
<section className="flex gap-2 py-4">
|
||||||
{sectionIds.map(section => (
|
{sectionIds.map(section => (
|
||||||
<Link
|
<Link
|
||||||
key={section}
|
key={section}
|
||||||
|
@ -8,5 +8,5 @@
|
|||||||
"@shared/*": ["shared/*"]
|
"@shared/*": ["shared/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["server", "*.config.*", "shared", "test"]
|
"include": ["server", "*.config.*", "shared", "test", "scripts"]
|
||||||
}
|
}
|
||||||
|