chore: file structure

This commit is contained in:
Ou 2024-10-01 13:01:01 +08:00
parent 3ec92ea3c8
commit ab35e2383d
5 changed files with 119 additions and 114 deletions

View File

@ -3,35 +3,51 @@ import type { UseQueryResult } from "@tanstack/react-query"
import { useQuery } from "@tanstack/react-query"
import { relativeTime } from "@shared/utils"
import clsx from "clsx"
import { CSS } from "@dnd-kit/utilities"
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 { useSortable } from "@dnd-kit/sortable"
import { focusSourcesAtom, refetchSourceAtom } from "~/atoms"
interface ItemsProps extends React.HTMLAttributes<HTMLDivElement> {
export interface ItemsProps extends React.HTMLAttributes<HTMLDivElement> {
id: SourceID
withOpacity?: boolean
isDragging?: boolean
/**
*
*/
isDragged?: boolean
isOverlay?: boolean
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: inViewRef, inView } = useInView({
threshold: 0,
})
useImperativeHandle(dndRef, () => ref.current as HTMLDivElement)
useImperativeHandle(inViewRef, () => ref.current as HTMLDivElement)
useImperativeHandle(dndRef, () => ref.current!)
useImperativeHandle(inViewRef, () => ref.current!)
return (
<div
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}
style={{
transformOrigin: "50% 50%",
@ -39,51 +55,71 @@ export const CardWrapper = forwardRef<HTMLDivElement, ItemsProps>(({ id, withOpa
}}
{...props}
>
<NewsCard id={id} inView={inView} isDragging={isDragging} listeners={listeners} />
<NewsCard id={id} inView={inView} isOverlay={isOverlay} handleListeners={listeners} />
</div>
)
})
export function SortableCardWrapper(props: ItemsProps) {
const { id } = props
const {
isDragging,
attributes,
listeners,
setNodeRef,
transform,
transition,
} = useSortable({ id })
export function NewsCard({ id, inView, isOverlay, handleListeners }: 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 style = {
transform: CSS.Transform.toString(transform),
transition: transition || undefined,
}
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 (
<CardWrapper
ref={setNodeRef}
style={style}
withOpacity={isDragging}
isDragging={isDragging}
listeners={listeners}
{...attributes}
{...props}
/>
<>
<div {...handleListeners} className={clsx("flex justify-between py-2 items-center", handleListeners && "cursor-grab", isOverlay && "cursor-grabbing")}>
<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>
</>
)
}
interface NewsCardProps {
id: SourceID
inView: boolean
isDragging?: boolean
listeners?: SyntheticListenerMap
}
interface Query {
query: UseQueryResult<SourceInfo, Error>
}
function SubTitle({ query }: Query) {
const subTitle = query.data?.type
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>
</>
)
}

View File

@ -9,14 +9,16 @@ import {
useSensor,
useSensors,
} 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 type { SourceID } from "@shared/types"
import { CSS } from "@dnd-kit/utilities"
import { GridContainer } from "./Pure"
import { CardWrapper, SortableCardWrapper } from "./Card"
import type { ItemsProps } from "./Card"
import { CardWrapper } from "./Card"
import { focusSourcesAtom } from "~/atoms"
export function Main() {
export function Dnd() {
const [items, setItems] = useAtom(focusSourcesAtom)
const [activeId, setActiveId] = useState<string | null>(null)
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor))
@ -58,8 +60,36 @@ export function Main() {
</GridContainer>
</SortableContext>
<DragOverlay adjustScale style={{ transformOrigin: "0 0 " }}>
{!!activeId && <CardWrapper id={activeId as SourceID} isDragging />}
{!!activeId && <CardWrapper id={activeId as SourceID} isOverlay />}
</DragOverlay>
</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}
/>
)
}

View File

@ -16,7 +16,7 @@ export function GridContainer({ children }: PropsWithChildren) {
)
}
export function Main() {
export function Pure() {
const currentSection = useAtomValue(currentSectionAtom)
return (
<GridContainer>

View File

@ -5,8 +5,8 @@ import clsx from "clsx"
import { useSetAtom } from "jotai"
import { useEffect } from "react"
import { currentSectionAtom } from "~/atoms"
import { Main as DndMain } from "~/components/Dnd"
import { Main as PureMain } from "~/components/Pure"
import { Dnd } from "~/components/section/Dnd"
import { Pure } from "~/components/section/Pure"
export const Route = createFileRoute("/")({
validateSearch: (search: any) => ({
@ -37,7 +37,7 @@ function IndexComponent() {
))}
</section>
{
id === "focus" ? <DndMain /> : <PureMain />
id === "focus" ? <Dnd /> : <Pure />
}
</div>
)

View File

@ -15,8 +15,7 @@ export default defineConfig({
"bg-base": "bg-white dark:bg-[#1d1c1c]",
"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-neutral-900:75 backdrop-blur-5",
"bg-glass": "bg-white:75 dark:bg-[#1d1c1c]:75 backdrop-blur-5",
"bg-code": "bg-gray5:5",
"bg-hover": "bg-primary-400:5",