feat: add source icon

This commit is contained in:
Ou 2024-10-01 00:08:07 +08:00
parent 346f2ee516
commit 3ec92ea3c8
41 changed files with 180 additions and 161 deletions

View File

@ -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
View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 879 B

BIN
public/icons/acfun.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
public/icons/baidu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
public/icons/bilibili.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
public/icons/douyin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
public/icons/genshin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
public/icons/github.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
public/icons/honkai.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
public/icons/hupu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
public/icons/ithome.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
public/icons/jianshu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

BIN
public/icons/juejin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

BIN
public/icons/kuaishou.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
public/icons/lol.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
public/icons/sspai.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

BIN
public/icons/starrail.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
public/icons/thepaper.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
public/icons/tieba.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

BIN
public/icons/toutiao.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

BIN
public/icons/v2ex.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

BIN
public/icons/weibo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

BIN
public/icons/weread.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

BIN
public/icons/zaobao.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
public/icons/zhihu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

49
scripts/favicon.ts Normal file
View 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
View File

0
server/sources/weibo.ts Normal file
View File

View 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: {

View File

@ -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[]
}

View File

@ -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())

View File

@ -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">

View File

@ -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}
/>
)
}

View File

@ -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))",
}}

View File

@ -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" />

View File

@ -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}

View File

@ -8,5 +8,5 @@
"@shared/*": ["shared/*"]
}
},
"include": ["server", "*.config.*", "shared", "test"]
"include": ["server", "*.config.*", "shared", "test", "scripts"]
}