feat: add source icon
@ -9,6 +9,7 @@
|
||||
"build": "vite build",
|
||||
"nitro": "nitro build",
|
||||
"lint": "eslint .",
|
||||
"favicon": "tsx ./scripts/favicon.ts",
|
||||
"test": "vitest",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
@ -21,12 +22,11 @@
|
||||
"@tanstack/react-router": "^1.58.9",
|
||||
"@tanstack/router-devtools": "^1.58.9",
|
||||
"@unocss/reset": "^0.62.4",
|
||||
"array-differences": "^3.0.2",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"framer-motion": "^11.8.0",
|
||||
"favicons-scraper": "^1.3.2",
|
||||
"h3": "^1.12.0",
|
||||
"jotai": "^2.10.0",
|
||||
"node-fetch": "^3.3.2",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-intersection-observer": "^9.13.1",
|
||||
@ -45,8 +45,8 @@
|
||||
"eslint": "^9.11.1",
|
||||
"eslint-plugin-react-hooks": "^5.1.0-rc-778e1ed2-20240926",
|
||||
"eslint-plugin-react-refresh": "^0.4.12",
|
||||
"globals": "^15.9.0",
|
||||
"nitropack": "^2.9.7",
|
||||
"tsx": "^4.19.1",
|
||||
"typescript": "^5.6.2",
|
||||
"typescript-eslint": "^8.7.0",
|
||||
"unocss": "^0.62.4",
|
||||
|
92
pnpm-lock.yaml
generated
@ -32,24 +32,21 @@ importers:
|
||||
'@unocss/reset':
|
||||
specifier: ^0.62.4
|
||||
version: 0.62.4
|
||||
array-differences:
|
||||
specifier: ^3.0.2
|
||||
version: 3.0.2
|
||||
clsx:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
dayjs:
|
||||
specifier: ^1.11.13
|
||||
version: 1.11.13
|
||||
framer-motion:
|
||||
specifier: ^11.8.0
|
||||
version: 11.8.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
favicons-scraper:
|
||||
specifier: ^1.3.2
|
||||
version: 1.3.2
|
||||
h3:
|
||||
specifier: ^1.12.0
|
||||
version: 1.12.0
|
||||
jotai:
|
||||
specifier: ^2.10.0
|
||||
version: 2.10.0(@types/react@18.3.9)(react@18.3.1)
|
||||
node-fetch:
|
||||
specifier: ^3.3.2
|
||||
version: 3.3.2
|
||||
react:
|
||||
specifier: ^18.3.1
|
||||
version: 18.3.1
|
||||
@ -99,12 +96,12 @@ importers:
|
||||
eslint-plugin-react-refresh:
|
||||
specifier: ^0.4.12
|
||||
version: 0.4.12(eslint@9.11.1(jiti@1.21.6))
|
||||
globals:
|
||||
specifier: ^15.9.0
|
||||
version: 15.9.0
|
||||
nitropack:
|
||||
specifier: ^2.9.7
|
||||
version: 2.9.7(magicast@0.3.5)
|
||||
tsx:
|
||||
specifier: ^4.19.1
|
||||
version: 4.19.1
|
||||
typescript:
|
||||
specifier: ^5.6.2
|
||||
version: 5.6.2
|
||||
@ -1799,9 +1796,6 @@ packages:
|
||||
argparse@2.0.1:
|
||||
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:
|
||||
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||
engines: {node: '>=12'}
|
||||
@ -2064,8 +2058,9 @@ packages:
|
||||
csstype@3.1.3:
|
||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||
|
||||
dayjs@1.11.13:
|
||||
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
|
||||
data-uri-to-buffer@4.0.1:
|
||||
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
|
||||
engines: {node: '>= 12'}
|
||||
|
||||
db0@0.1.4:
|
||||
resolution: {integrity: sha512-Ft6eCwONYxlwLjBXSJxw0t0RYtA5gW9mq8JfBXn9TtC0nDPlqePAhpv9v4g9aONBi6JI1OXHTKKkUYGd+BOrCA==}
|
||||
@ -2554,6 +2549,9 @@ packages:
|
||||
fastq@1.17.1:
|
||||
resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
|
||||
|
||||
favicons-scraper@1.3.2:
|
||||
resolution: {integrity: sha512-rsEBzLyEdJFEg/fzAx0vl8HypWwjSxisdHTaZ1W/jrKRuCm+0GG4xVWrOfhvG18dAhG2eKBW0tfhGarQt/HL8w==}
|
||||
|
||||
fdir@6.3.0:
|
||||
resolution: {integrity: sha512-QOnuT+BOtivR77wYvCWHfGt9s4Pz1VIMbD463vegT5MLqNXy8rYFT/lPVEqf/bhYeT6qmqrNHhsX+rWwe3rOCQ==}
|
||||
peerDependencies:
|
||||
@ -2562,6 +2560,10 @@ packages:
|
||||
picomatch:
|
||||
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:
|
||||
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
@ -2596,19 +2598,9 @@ packages:
|
||||
resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
framer-motion@11.8.0:
|
||||
resolution: {integrity: sha512-q/axN/PFRdKmzPK6PO2OhbLUMWXXZuiejdM1/3FhC2hm4YIetc+qeco2EvWm4u1/UTFmevclE492wGFNfSZ4eQ==}
|
||||
peerDependencies:
|
||||
'@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
|
||||
formdata-polyfill@4.0.10:
|
||||
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
|
||||
fresh@0.5.2:
|
||||
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
|
||||
@ -3207,6 +3199,10 @@ packages:
|
||||
node-addon-api@7.1.1:
|
||||
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:
|
||||
resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==}
|
||||
|
||||
@ -3219,6 +3215,10 @@ packages:
|
||||
encoding:
|
||||
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:
|
||||
resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==}
|
||||
engines: {node: '>= 6.13.0'}
|
||||
@ -4180,6 +4180,10 @@ packages:
|
||||
peerDependencies:
|
||||
eslint: '>=6.0.0'
|
||||
|
||||
web-streams-polyfill@3.3.3:
|
||||
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
webidl-conversions@3.0.1:
|
||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||
|
||||
@ -5859,8 +5863,6 @@ snapshots:
|
||||
|
||||
argparse@2.0.1: {}
|
||||
|
||||
array-differences@3.0.2: {}
|
||||
|
||||
assertion-error@2.0.1: {}
|
||||
|
||||
async-sema@3.1.1: {}
|
||||
@ -6110,7 +6112,7 @@ snapshots:
|
||||
|
||||
csstype@3.1.3: {}
|
||||
|
||||
dayjs@1.11.13: {}
|
||||
data-uri-to-buffer@4.0.1: {}
|
||||
|
||||
db0@0.1.4: {}
|
||||
|
||||
@ -6769,10 +6771,17 @@ snapshots:
|
||||
dependencies:
|
||||
reusify: 1.0.4
|
||||
|
||||
favicons-scraper@1.3.2: {}
|
||||
|
||||
fdir@6.3.0(picomatch@4.0.2):
|
||||
optionalDependencies:
|
||||
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:
|
||||
dependencies:
|
||||
flat-cache: 4.0.1
|
||||
@ -6807,12 +6816,9 @@ snapshots:
|
||||
cross-spawn: 7.0.3
|
||||
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:
|
||||
tslib: 2.7.0
|
||||
optionalDependencies:
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
fetch-blob: 3.2.0
|
||||
|
||||
fresh@0.5.2: {}
|
||||
|
||||
@ -7486,12 +7492,20 @@ snapshots:
|
||||
|
||||
node-addon-api@7.1.1: {}
|
||||
|
||||
node-domexception@1.0.0: {}
|
||||
|
||||
node-fetch-native@1.6.4: {}
|
||||
|
||||
node-fetch@2.7.0:
|
||||
dependencies:
|
||||
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-gyp-build@4.8.2: {}
|
||||
@ -8496,6 +8510,8 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
web-streams-polyfill@3.3.3: {}
|
||||
|
||||
webidl-conversions@3.0.1: {}
|
||||
|
||||
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 sourceList = {
|
||||
"36kr": "36氪",
|
||||
"douyin": "抖音",
|
||||
"hupu": "虎扑",
|
||||
"zhihu": "知乎",
|
||||
"weibo": "微博",
|
||||
"tieba": "贴吧",
|
||||
"zaobao": "联合早报",
|
||||
"thepaper": "澎湃新闻",
|
||||
"toutiao": "今日头条",
|
||||
"cankaoxiaoxi": "参考消息",
|
||||
"peopledaily": "人民日报",
|
||||
} as const satisfies Record<string, string | false>
|
||||
export const sources = {
|
||||
"36kr": {
|
||||
name: "36氪",
|
||||
home: "https://36kr.com",
|
||||
},
|
||||
"douyin": {
|
||||
name: "抖音",
|
||||
home: "https://www.douyin.com",
|
||||
},
|
||||
"hupu": {
|
||||
name: "虎扑",
|
||||
home: "https://hupu.com",
|
||||
},
|
||||
"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 = {
|
||||
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 Metadata = Record<SectionID, Section>
|
||||
|
||||
@ -29,6 +29,5 @@ export interface SourceInfo {
|
||||
total: number
|
||||
link?: string
|
||||
updateTime: string
|
||||
fromCache: boolean
|
||||
data: NewsItem[]
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { atom } from "jotai"
|
||||
import type { SectionID, SourceID } from "@shared/types"
|
||||
import { metadata, sourceList } from "@shared/data"
|
||||
import { metadata, sources } from "@shared/data"
|
||||
import { atomWithLocalStorage } from "./hooks/atomWithLocalStorage"
|
||||
|
||||
export const focusSourcesAtom = atomWithLocalStorage<SourceID[]>("focusSources", [], (stored) => {
|
||||
return stored.filter(item => item in sourceList)
|
||||
return stored.filter(item => item in sources)
|
||||
})
|
||||
|
||||
function initRefetchSource() {
|
||||
@ -17,7 +17,7 @@ function initRefetchSource() {
|
||||
if (!Number.isNaN(quitTime) && now - quitTime < 1000) {
|
||||
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())
|
||||
|
@ -6,8 +6,8 @@ import clsx from "clsx"
|
||||
import { CSS } from "@dnd-kit/utilities"
|
||||
import { useInView } from "react-intersection-observer"
|
||||
import { useAtom } from "jotai"
|
||||
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef } from "react"
|
||||
import { sourceList } from "@shared/data"
|
||||
import { forwardRef, useCallback, useImperativeHandle, useRef } from "react"
|
||||
import { sources } from "@shared/data"
|
||||
import type { SyntheticListenerMap } from "@dnd-kit/core/dist/hooks/utilities"
|
||||
import { useSortable } from "@dnd-kit/sortable"
|
||||
import { focusSourcesAtom, refetchSourceAtom } from "~/atoms"
|
||||
@ -25,22 +25,13 @@ export const CardWrapper = forwardRef<HTMLDivElement, ItemsProps>(({ id, withOpa
|
||||
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(inViewRef, () => ref.current as HTMLDivElement)
|
||||
|
||||
return (
|
||||
<div
|
||||
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}
|
||||
style={{
|
||||
transformOrigin: "50% 50%",
|
||||
@ -95,7 +86,7 @@ interface Query {
|
||||
|
||||
function SubTitle({ query }: Query) {
|
||||
const subTitle = query.data?.type
|
||||
if (subTitle) return <span>{subTitle}</span>
|
||||
if (subTitle) return <span className="text-xs">{subTitle}</span>
|
||||
}
|
||||
|
||||
function UpdateTime({ query }: Query) {
|
||||
@ -108,7 +99,7 @@ function UpdateTime({ query }: Query) {
|
||||
function Num({ num }: { num: number }) {
|
||||
const color = ["bg-red-900", "bg-red-500", "bg-red-400"]
|
||||
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}
|
||||
</span>
|
||||
)
|
||||
@ -122,12 +113,12 @@ function NewsList({ query }: Query) {
|
||||
{items.slice(0, 20).map((item, i) => (
|
||||
<div key={item.title} className="flex gap-2 items-center">
|
||||
<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">
|
||||
<span>
|
||||
<a href={item.url} target="_blank" className="my-1 w-full flex items-center justify-between flex-wrap">
|
||||
<span className="flex-1 mr-2">
|
||||
{item.title}
|
||||
</span>
|
||||
{item.timestamp && (
|
||||
<span className="text-xs">
|
||||
<span className="text-xs text-gray-4/80">
|
||||
{relativeTime(item.timestamp)}
|
||||
</span>
|
||||
)}
|
||||
@ -182,10 +173,13 @@ export function NewsCard({ id, inView, isDragging, listeners }: NewsCardProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div {...listeners} className={clsx("flex justify-between py-2", isDragging ? "cursor-grabbing" : "cursor-grab")}>
|
||||
<span className="text-lg font-bold">
|
||||
{sourceList[id]}
|
||||
</span>
|
||||
<div {...listeners} className={clsx("flex justify-between py-2 items-center", listeners && (isDragging ? "cursor-grabbing" : "cursor-grab"))}>
|
||||
<div className="flex items-center gap-2">
|
||||
<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>
|
||||
</div>
|
||||
<SubTitle query={query} />
|
||||
</div>
|
||||
<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) {
|
||||
return (
|
||||
<div
|
||||
className="grid w-full gap-5 mt-10"
|
||||
className="grid w-full gap-5"
|
||||
style={{
|
||||
gridTemplateColumns: "repeat(auto-fill, minmax(350px, 1fr))",
|
||||
}}
|
||||
|
@ -24,7 +24,7 @@ export const Route = createRootRouteWithContext<{
|
||||
export function RootComponent() {
|
||||
useOnReload()
|
||||
return (
|
||||
<div className="p-10">
|
||||
<div className="md:p-10 p-4">
|
||||
<Header />
|
||||
<Outlet />
|
||||
<ReactQueryDevtools buttonPosition="bottom-left" />
|
||||
|
@ -24,7 +24,7 @@ function IndexComponent() {
|
||||
|
||||
return id && (
|
||||
<div className="flex flex-col justify-center items-center">
|
||||
<section className="flex gap-2">
|
||||
<section className="flex gap-2 py-4">
|
||||
{sectionIds.map(section => (
|
||||
<Link
|
||||
key={section}
|
||||
|
@ -8,5 +8,5 @@
|
||||
"@shared/*": ["shared/*"]
|
||||
}
|
||||
},
|
||||
"include": ["server", "*.config.*", "shared", "test"]
|
||||
"include": ["server", "*.config.*", "shared", "test", "scripts"]
|
||||
}
|
||||
|