mirror of
https://github.com/ourongxing/newsnow.git
synced 2025-01-31 10:58:04 +08:00
chore: remove git cache
This commit is contained in:
parent
be19adb4bc
commit
309ab3b2e1
@ -1,49 +0,0 @@
|
|||||||
import { Link } from "@tanstack/react-router"
|
|
||||||
import { useCallback } from "react"
|
|
||||||
import { useAtomValue, useSetAtom } from "jotai"
|
|
||||||
import logo from "~/assets/react.svg"
|
|
||||||
import { useDark } from "~/hooks/useDark"
|
|
||||||
import { currentSectionAtom, refetchSourcesAtom } from "~/atoms"
|
|
||||||
|
|
||||||
function ThemeToggle() {
|
|
||||||
const { toggleDark } = useDark()
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
title="Toggle Dark Mode"
|
|
||||||
className="i-ph-sun-dim-duotone dark:i-ph-moon-stars-duotone btn-pure"
|
|
||||||
onClick={toggleDark}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function RefreshButton() {
|
|
||||||
const currentSection = useAtomValue(currentSectionAtom)
|
|
||||||
const setRefetchSource = useSetAtom(refetchSourcesAtom)
|
|
||||||
const refreshAll = useCallback(() => {
|
|
||||||
const obj = Object.fromEntries(currentSection.sourceList.map(id => [id, Date.now()]))
|
|
||||||
setRefetchSource(prev => ({
|
|
||||||
...prev,
|
|
||||||
...obj,
|
|
||||||
}))
|
|
||||||
}, [currentSection, setRefetchSource])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button type="button" className="i-ph:arrow-clockwise btn-pure" onClick={refreshAll} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Header() {
|
|
||||||
return (
|
|
||||||
<header className="flex justify-between items-center">
|
|
||||||
<Link className="text-6 flex gap-2 items-center" to="/">
|
|
||||||
<img src={logo} alt="logo" className="h-8" />
|
|
||||||
<span className="font-mono">NewsNow</span>
|
|
||||||
</Link>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<RefreshButton />
|
|
||||||
<ThemeToggle />
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,199 +0,0 @@
|
|||||||
import type { NewsItem, SourceID, SourceInfo, SourceResponse } from "@shared/types"
|
|
||||||
import { OverlayScrollbarsComponent } from "overlayscrollbars-react"
|
|
||||||
import type { UseQueryResult } from "@tanstack/react-query"
|
|
||||||
import { useQuery } from "@tanstack/react-query"
|
|
||||||
import clsx from "clsx"
|
|
||||||
import { useInView } from "react-intersection-observer"
|
|
||||||
import { useAtom } from "jotai"
|
|
||||||
import { forwardRef, useCallback, useImperativeHandle, useRef } from "react"
|
|
||||||
import { sources } from "@shared/data"
|
|
||||||
import type { SyntheticListenerMap } from "@dnd-kit/core/dist/hooks/utilities"
|
|
||||||
import { focusSourcesAtom, refetchSourcesAtom } from "~/atoms"
|
|
||||||
import { useRelativeTime } from "~/hooks/useRelativeTime"
|
|
||||||
|
|
||||||
export interface ItemsProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
||||||
id: SourceID
|
|
||||||
/**
|
|
||||||
* 是否显示透明度,拖动时原卡片的样式
|
|
||||||
*/
|
|
||||||
isDragged?: boolean
|
|
||||||
isOverlay?: boolean
|
|
||||||
handleListeners?: SyntheticListenerMap
|
|
||||||
}
|
|
||||||
|
|
||||||
interface NewsCardProps {
|
|
||||||
id: SourceID
|
|
||||||
inView: boolean
|
|
||||||
isOverlay?: boolean
|
|
||||||
handleListeners?: SyntheticListenerMap
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Query {
|
|
||||||
query: UseQueryResult<SourceInfo, Error>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CardWrapper = forwardRef<HTMLDivElement, ItemsProps>(({ id, isDragged, isOverlay, handleListeners, style, ...props }, dndRef) => {
|
|
||||||
const ref = useRef<HTMLDivElement>(null)
|
|
||||||
const { ref: inViewRef, inView } = useInView({
|
|
||||||
threshold: 0,
|
|
||||||
})
|
|
||||||
|
|
||||||
useImperativeHandle(dndRef, () => ref.current!)
|
|
||||||
useImperativeHandle(inViewRef, () => ref.current!)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className={clsx(
|
|
||||||
"flex flex-col h-500px aspect-auto border border-gray-100 rounded-xl shadow-2xl shadow-gray-600/10 bg-base dark:( border-gray-700 shadow-none)",
|
|
||||||
isDragged && "op-50",
|
|
||||||
isOverlay ? "bg-glass" : "",
|
|
||||||
)}
|
|
||||||
style={{
|
|
||||||
transformOrigin: "50% 50%",
|
|
||||||
...style,
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<NewsCard id={id} inView={inView} isOverlay={isOverlay} handleListeners={handleListeners} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
export function NewsCard({ id, inView, isOverlay, handleListeners }: NewsCardProps) {
|
|
||||||
const [focusSources, setFocusSources] = useAtom(focusSourcesAtom)
|
|
||||||
const [refetchSource, setRefetchSource] = useAtom(refetchSourcesAtom)
|
|
||||||
const query = useQuery({
|
|
||||||
queryKey: [id, refetchSource[id]],
|
|
||||||
queryFn: async ({ queryKey }) => {
|
|
||||||
const [_id, _refetchTime] = queryKey as [SourceID, number]
|
|
||||||
let url = `/api/${_id}`
|
|
||||||
if (Date.now() - _refetchTime < 1000) {
|
|
||||||
url = `/api/${_id}?latest`
|
|
||||||
}
|
|
||||||
const response: SourceResponse = await fetch(url).then(res => res.json())
|
|
||||||
if (response.status === "error") {
|
|
||||||
throw new Error(response.message)
|
|
||||||
} else {
|
|
||||||
return response.data
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// refetch 时显示原有的数据
|
|
||||||
placeholderData: prev => 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,
|
|
||||||
[id]: Date.now(),
|
|
||||||
}))
|
|
||||||
}, [setRefetchSource, id])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
{...handleListeners}
|
|
||||||
className={clsx([
|
|
||||||
"flex justify-between p-2 items-center",
|
|
||||||
handleListeners && "cursor-grab",
|
|
||||||
isOverlay && "cursor-grabbing",
|
|
||||||
])}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<img src={`/icons/${id.split("-")[0]}.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>
|
|
||||||
{/* @ts-expect-error -_- */}
|
|
||||||
<span className="text-xs">{sources[id]?.type}</span>
|
|
||||||
</div>
|
|
||||||
<OverlayScrollbarsComponent
|
|
||||||
defer
|
|
||||||
className="h-full pl-2 pr-3 mr-1"
|
|
||||||
element="div"
|
|
||||||
options={{ scrollbars: { autoHide: "scroll" }, overflow: { x: "hidden" } }}
|
|
||||||
>
|
|
||||||
<NewsList query={query} />
|
|
||||||
</OverlayScrollbarsComponent>
|
|
||||||
<div className="p-2 flex items-center justify-between">
|
|
||||||
<UpdateTime query={query} />
|
|
||||||
<div className="flex gap-1">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={clsx("i-ph:arrow-clockwise", query.isFetching && "animate-spin")}
|
|
||||||
onClick={manualRefetch}
|
|
||||||
/>
|
|
||||||
<button type="button" className={clsx(focusSources.includes(id) ? "i-ph:star-fill" : "i-ph:star", "color-primary")} onClick={addFocusList} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function UpdateTime({ query }: Query) {
|
|
||||||
const updatedTime = useRelativeTime(query.data?.updatedTime ?? "")
|
|
||||||
if (updatedTime) return <span>{`${updatedTime}更新`}</span>
|
|
||||||
if (query.isError) return <span>获取失败</span>
|
|
||||||
return <span className="skeleton w-20" />
|
|
||||||
}
|
|
||||||
|
|
||||||
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", false && color[num - 1])}>
|
|
||||||
{num}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ExtraInfo({ item }: { item: NewsItem }) {
|
|
||||||
const relativeTime = useRelativeTime(item?.extra?.date)
|
|
||||||
if (relativeTime) {
|
|
||||||
return <>{relativeTime}</>
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item?.extra?.icon) {
|
|
||||||
return (
|
|
||||||
<img src={item.extra.icon} className="w-5 inline" />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function NewsList({ query }: Query) {
|
|
||||||
const items = query.data?.items
|
|
||||||
if (items?.length) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{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">
|
|
||||||
<span className="mr-2">
|
|
||||||
{item.title}
|
|
||||||
</span>
|
|
||||||
<span className="text-xs text-gray-4/80 truncate align-middle">
|
|
||||||
<ExtraInfo item={item} />
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{Array.from({ length: 20 }).map((_, i) => i).map(i => (
|
|
||||||
<div key={i} className="flex gap-2 items-center">
|
|
||||||
<Num num={i + 1} />
|
|
||||||
<span className="skeleton border-b border-gray-300/20 my-1"></span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,92 +0,0 @@
|
|||||||
import { useCallback, useState } from "react"
|
|
||||||
import type { DragEndEvent, DragStartEvent } from "@dnd-kit/core"
|
|
||||||
import {
|
|
||||||
DndContext,
|
|
||||||
DragOverlay,
|
|
||||||
MouseSensor,
|
|
||||||
TouchSensor,
|
|
||||||
closestCenter,
|
|
||||||
useSensor,
|
|
||||||
useSensors,
|
|
||||||
} from "@dnd-kit/core"
|
|
||||||
import { SortableContext, arrayMove, rectSortingStrategy, useSortable } from "@dnd-kit/sortable"
|
|
||||||
import { useAtom } from "jotai"
|
|
||||||
import type { SourceID } from "@shared/types"
|
|
||||||
import { CSS } from "@dnd-kit/utilities"
|
|
||||||
import type { ItemsProps } from "./card"
|
|
||||||
import { CardWrapper } from "./card"
|
|
||||||
import { focusSourcesAtom } from "~/atoms"
|
|
||||||
|
|
||||||
export function Dnd() {
|
|
||||||
const [items, setItems] = useAtom(focusSourcesAtom)
|
|
||||||
const [activeId, setActiveId] = useState<string | null>(null)
|
|
||||||
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor))
|
|
||||||
|
|
||||||
const handleDragStart = useCallback((event: DragStartEvent) => {
|
|
||||||
setActiveId(event.active.id as string)
|
|
||||||
}, [])
|
|
||||||
const handleDragEnd = useCallback((event: DragEndEvent) => {
|
|
||||||
const { active, over } = event
|
|
||||||
|
|
||||||
if (active.id !== over?.id) {
|
|
||||||
setItems((items) => {
|
|
||||||
const oldIndex = items.indexOf(active.id as any)
|
|
||||||
const newIndex = items.indexOf(over!.id as any)
|
|
||||||
|
|
||||||
return arrayMove(items, oldIndex, newIndex)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
setActiveId(null)
|
|
||||||
}, [setItems])
|
|
||||||
const handleDragCancel = useCallback(() => {
|
|
||||||
setActiveId(null)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DndContext
|
|
||||||
sensors={sensors}
|
|
||||||
collisionDetection={closestCenter}
|
|
||||||
onDragStart={handleDragStart}
|
|
||||||
onDragEnd={handleDragEnd}
|
|
||||||
onDragCancel={handleDragCancel}
|
|
||||||
>
|
|
||||||
<SortableContext items={items} strategy={rectSortingStrategy}>
|
|
||||||
{items.map(id => (
|
|
||||||
<SortableCardWrapper key={id} id={id} />
|
|
||||||
))}
|
|
||||||
</SortableContext>
|
|
||||||
<DragOverlay adjustScale style={{ transformOrigin: "0 0 " }}>
|
|
||||||
{!!activeId && <CardWrapper id={activeId as SourceID} isOverlay />}
|
|
||||||
</DragOverlay>
|
|
||||||
</DndContext>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SortableCardWrapper({ id, ...props }: ItemsProps) {
|
|
||||||
const {
|
|
||||||
isDragging,
|
|
||||||
attributes,
|
|
||||||
listeners,
|
|
||||||
setNodeRef,
|
|
||||||
transform,
|
|
||||||
transition,
|
|
||||||
} = useSortable({ id })
|
|
||||||
|
|
||||||
const style = {
|
|
||||||
transform: CSS.Transform.toString(transform),
|
|
||||||
transition,
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CardWrapper
|
|
||||||
ref={setNodeRef}
|
|
||||||
id={id}
|
|
||||||
style={style}
|
|
||||||
isDragged={isDragging}
|
|
||||||
handleListeners={listeners}
|
|
||||||
{...attributes}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user