mirror of
https://github.com/ourongxing/newsnow.git
synced 2025-01-19 03:09:14 +08:00
chore: file structure
This commit is contained in:
parent
3ec92ea3c8
commit
ab35e2383d
@ -3,35 +3,51 @@ import type { UseQueryResult } from "@tanstack/react-query"
|
|||||||
import { useQuery } from "@tanstack/react-query"
|
import { useQuery } from "@tanstack/react-query"
|
||||||
import { relativeTime } from "@shared/utils"
|
import { relativeTime } from "@shared/utils"
|
||||||
import clsx from "clsx"
|
import clsx from "clsx"
|
||||||
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, useImperativeHandle, useRef } from "react"
|
import { forwardRef, useCallback, useImperativeHandle, useRef } from "react"
|
||||||
import { sources } 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 { focusSourcesAtom, refetchSourceAtom } from "~/atoms"
|
import { focusSourcesAtom, refetchSourceAtom } from "~/atoms"
|
||||||
|
|
||||||
interface ItemsProps extends React.HTMLAttributes<HTMLDivElement> {
|
export interface ItemsProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
id: SourceID
|
id: SourceID
|
||||||
withOpacity?: boolean
|
/**
|
||||||
isDragging?: boolean
|
* 是否显示透明度,拖动时原卡片的样式
|
||||||
|
*/
|
||||||
|
isDragged?: boolean
|
||||||
|
isOverlay?: boolean
|
||||||
listeners?: SyntheticListenerMap
|
listeners?: SyntheticListenerMap
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CardWrapper = forwardRef<HTMLDivElement, ItemsProps>(({ id, withOpacity, isDragging, listeners, style, ...props }, dndRef) => {
|
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, listeners, style, ...props }, dndRef) => {
|
||||||
const ref = useRef<HTMLDivElement>(null)
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
const { ref: inViewRef, inView } = useInView({
|
const { ref: inViewRef, inView } = useInView({
|
||||||
threshold: 0,
|
threshold: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
useImperativeHandle(dndRef, () => ref.current as HTMLDivElement)
|
useImperativeHandle(dndRef, () => ref.current!)
|
||||||
useImperativeHandle(inViewRef, () => ref.current as HTMLDivElement)
|
useImperativeHandle(inViewRef, () => ref.current!)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={clsx("flex flex-col bg-base border rounded-md px-2 h-500px", withOpacity && "op-50", isDragging ? "" : "")}
|
className={clsx(
|
||||||
|
"flex flex-col bg-base border rounded-md px-2 h-500px",
|
||||||
|
isDragged && "op-50",
|
||||||
|
isOverlay ? "bg-glass" : "",
|
||||||
|
)}
|
||||||
key={id}
|
key={id}
|
||||||
style={{
|
style={{
|
||||||
transformOrigin: "50% 50%",
|
transformOrigin: "50% 50%",
|
||||||
@ -39,51 +55,71 @@ export const CardWrapper = forwardRef<HTMLDivElement, ItemsProps>(({ id, withOpa
|
|||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<NewsCard id={id} inView={inView} isDragging={isDragging} listeners={listeners} />
|
<NewsCard id={id} inView={inView} isOverlay={isOverlay} handleListeners={listeners} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export function SortableCardWrapper(props: ItemsProps) {
|
export function NewsCard({ id, inView, isOverlay, handleListeners }: NewsCardProps) {
|
||||||
const { id } = props
|
const [focusSources, setFocusSources] = useAtom(focusSourcesAtom)
|
||||||
const {
|
const [refetchSource, setRefetchSource] = useAtom(refetchSourceAtom)
|
||||||
isDragging,
|
const query = useQuery({
|
||||||
attributes,
|
queryKey: [id, refetchSource[id]],
|
||||||
listeners,
|
queryFn: async ({ queryKey }) => {
|
||||||
setNodeRef,
|
const [_id, _refetchTime] = queryKey as [SourceID, number]
|
||||||
transform,
|
let url = `/api/${_id}`
|
||||||
transition,
|
if (Date.now() - _refetchTime < 1000) {
|
||||||
} = useSortable({ id })
|
url = `/api/${_id}?latest`
|
||||||
|
|
||||||
const style = {
|
|
||||||
transform: CSS.Transform.toString(transform),
|
|
||||||
transition: transition || undefined,
|
|
||||||
}
|
}
|
||||||
|
const response = await fetch(url)
|
||||||
|
return await response.json() as SourceInfo
|
||||||
|
},
|
||||||
|
// refetch 时显示原有的数据
|
||||||
|
placeholderData: prev => prev,
|
||||||
|
staleTime: 1000 * 60 * 5,
|
||||||
|
enabled: inView,
|
||||||
|
refetchOnWindowFocus: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<CardWrapper
|
<>
|
||||||
ref={setNodeRef}
|
<div {...handleListeners} className={clsx("flex justify-between py-2 items-center", handleListeners && "cursor-grab", isOverlay && "cursor-grabbing")}>
|
||||||
style={style}
|
<div className="flex items-center gap-2">
|
||||||
withOpacity={isDragging}
|
<img src={`/icons/${id}.png`} className="w-4 h-4 rounded" alt={id} onError={e => e.currentTarget.hidden = true} />
|
||||||
isDragging={isDragging}
|
<span className="text-md font-bold">
|
||||||
listeners={listeners}
|
{sources[id].name}
|
||||||
{...attributes}
|
</span>
|
||||||
{...props}
|
</div>
|
||||||
|
<SubTitle query={query} />
|
||||||
|
</div>
|
||||||
|
<div className="overflow-auto h-full">
|
||||||
|
<NewsList query={query} />
|
||||||
|
</div>
|
||||||
|
<div className="py-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")} onClick={addFocusList} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NewsCardProps {
|
|
||||||
id: SourceID
|
|
||||||
inView: boolean
|
|
||||||
isDragging?: boolean
|
|
||||||
listeners?: SyntheticListenerMap
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Query {
|
|
||||||
query: UseQueryResult<SourceInfo, Error>
|
|
||||||
}
|
|
||||||
|
|
||||||
function SubTitle({ query }: Query) {
|
function SubTitle({ query }: Query) {
|
||||||
const subTitle = query.data?.type
|
const subTitle = query.data?.type
|
||||||
if (subTitle) return <span className="text-xs">{subTitle}</span>
|
if (subTitle) return <span className="text-xs">{subTitle}</span>
|
||||||
@ -139,63 +175,3 @@ function NewsList({ query }: Query) {
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NewsCard({ id, inView, isDragging, listeners }: NewsCardProps) {
|
|
||||||
const [focusSources, setFocusSources] = useAtom(focusSourcesAtom)
|
|
||||||
const [refetchSource, setRefetchSource] = useAtom(refetchSourceAtom)
|
|
||||||
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 = await fetch(url)
|
|
||||||
return await response.json() as SourceInfo
|
|
||||||
},
|
|
||||||
// refetch 时显示原有的数据
|
|
||||||
placeholderData: prev => prev,
|
|
||||||
staleTime: 1000 * 60 * 5,
|
|
||||||
enabled: inView,
|
|
||||||
refetchOnWindowFocus: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
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 {...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">
|
|
||||||
<NewsList query={query} />
|
|
||||||
</div>
|
|
||||||
<div className="py-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")} onClick={addFocusList} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
@ -9,14 +9,16 @@ import {
|
|||||||
useSensor,
|
useSensor,
|
||||||
useSensors,
|
useSensors,
|
||||||
} from "@dnd-kit/core"
|
} from "@dnd-kit/core"
|
||||||
import { SortableContext, arrayMove, rectSortingStrategy } from "@dnd-kit/sortable"
|
import { SortableContext, arrayMove, rectSortingStrategy, useSortable } from "@dnd-kit/sortable"
|
||||||
import { useAtom } from "jotai"
|
import { useAtom } from "jotai"
|
||||||
import type { SourceID } from "@shared/types"
|
import type { SourceID } from "@shared/types"
|
||||||
|
import { CSS } from "@dnd-kit/utilities"
|
||||||
import { GridContainer } from "./Pure"
|
import { GridContainer } from "./Pure"
|
||||||
import { CardWrapper, SortableCardWrapper } from "./Card"
|
import type { ItemsProps } from "./Card"
|
||||||
|
import { CardWrapper } from "./Card"
|
||||||
import { focusSourcesAtom } from "~/atoms"
|
import { focusSourcesAtom } from "~/atoms"
|
||||||
|
|
||||||
export function Main() {
|
export function Dnd() {
|
||||||
const [items, setItems] = useAtom(focusSourcesAtom)
|
const [items, setItems] = useAtom(focusSourcesAtom)
|
||||||
const [activeId, setActiveId] = useState<string | null>(null)
|
const [activeId, setActiveId] = useState<string | null>(null)
|
||||||
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor))
|
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor))
|
||||||
@ -58,8 +60,36 @@ export function Main() {
|
|||||||
</GridContainer>
|
</GridContainer>
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
<DragOverlay adjustScale style={{ transformOrigin: "0 0 " }}>
|
<DragOverlay adjustScale style={{ transformOrigin: "0 0 " }}>
|
||||||
{!!activeId && <CardWrapper id={activeId as SourceID} isDragging />}
|
{!!activeId && <CardWrapper id={activeId as SourceID} isOverlay />}
|
||||||
</DragOverlay>
|
</DragOverlay>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function SortableCardWrapper(props: ItemsProps) {
|
||||||
|
const { id } = props
|
||||||
|
const {
|
||||||
|
isDragging,
|
||||||
|
attributes,
|
||||||
|
listeners,
|
||||||
|
setNodeRef,
|
||||||
|
transform,
|
||||||
|
transition,
|
||||||
|
} = useSortable({ id })
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition: transition || undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CardWrapper
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={style}
|
||||||
|
isDragged={isDragging}
|
||||||
|
listeners={listeners}
|
||||||
|
{...attributes}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
@ -16,7 +16,7 @@ export function GridContainer({ children }: PropsWithChildren) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Main() {
|
export function Pure() {
|
||||||
const currentSection = useAtomValue(currentSectionAtom)
|
const currentSection = useAtomValue(currentSectionAtom)
|
||||||
return (
|
return (
|
||||||
<GridContainer>
|
<GridContainer>
|
@ -5,8 +5,8 @@ import clsx from "clsx"
|
|||||||
import { useSetAtom } from "jotai"
|
import { useSetAtom } from "jotai"
|
||||||
import { useEffect } from "react"
|
import { useEffect } from "react"
|
||||||
import { currentSectionAtom } from "~/atoms"
|
import { currentSectionAtom } from "~/atoms"
|
||||||
import { Main as DndMain } from "~/components/Dnd"
|
import { Dnd } from "~/components/section/Dnd"
|
||||||
import { Main as PureMain } from "~/components/Pure"
|
import { Pure } from "~/components/section/Pure"
|
||||||
|
|
||||||
export const Route = createFileRoute("/")({
|
export const Route = createFileRoute("/")({
|
||||||
validateSearch: (search: any) => ({
|
validateSearch: (search: any) => ({
|
||||||
@ -37,7 +37,7 @@ function IndexComponent() {
|
|||||||
))}
|
))}
|
||||||
</section>
|
</section>
|
||||||
{
|
{
|
||||||
id === "focus" ? <DndMain /> : <PureMain />
|
id === "focus" ? <Dnd /> : <Pure />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -15,8 +15,7 @@ export default defineConfig({
|
|||||||
"bg-base": "bg-white dark:bg-[#1d1c1c]",
|
"bg-base": "bg-white dark:bg-[#1d1c1c]",
|
||||||
"border-base": "border-gray-500/30",
|
"border-base": "border-gray-500/30",
|
||||||
|
|
||||||
"bg-tooltip": "bg-white:75 dark:bg-neutral-900:75 backdrop-blur-8",
|
"bg-glass": "bg-white:75 dark:bg-[#1d1c1c]:75 backdrop-blur-5",
|
||||||
"bg-glass": "bg-white:75 dark:bg-neutral-900:75 backdrop-blur-5",
|
|
||||||
"bg-code": "bg-gray5:5",
|
"bg-code": "bg-gray5:5",
|
||||||
"bg-hover": "bg-primary-400:5",
|
"bg-hover": "bg-primary-400:5",
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user