mirror of
https://github.com/ourongxing/newsnow.git
synced 2025-01-19 11:19:14 +08:00
refactor: router
This commit is contained in:
parent
8ca82048de
commit
be19adb4bc
@ -7,7 +7,7 @@ export const focusSourcesAtom = atomWithLocalStorage<SourceID[]>("focusSources",
|
|||||||
return stored.filter(item => item in sources)
|
return stored.filter(item => item in sources)
|
||||||
})
|
})
|
||||||
|
|
||||||
function initRefetchSource() {
|
function initRefetchSources() {
|
||||||
let time = 0
|
let time = 0
|
||||||
// useOnReload
|
// useOnReload
|
||||||
// 没有放在 useOnReload 里面, 可以避免初始化后再修改 refetchSourceAtom,导致多次请求 API
|
// 没有放在 useOnReload 里面, 可以避免初始化后再修改 refetchSourceAtom,导致多次请求 API
|
||||||
@ -20,7 +20,7 @@ function initRefetchSource() {
|
|||||||
return Object.fromEntries(Object.keys(sources).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())
|
export const refetchSourcesAtom = atom(initRefetchSources())
|
||||||
|
|
||||||
export const currentSectionIDAtom = atom<SectionID>("focus")
|
export const currentSectionIDAtom = atom<SectionID>("focus")
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import { useCallback } from "react"
|
|||||||
import { useAtomValue, useSetAtom } from "jotai"
|
import { useAtomValue, useSetAtom } from "jotai"
|
||||||
import logo from "~/assets/react.svg"
|
import logo from "~/assets/react.svg"
|
||||||
import { useDark } from "~/hooks/useDark"
|
import { useDark } from "~/hooks/useDark"
|
||||||
import { currentSectionAtom, refetchSourceAtom } from "~/atoms"
|
import { currentSectionAtom, refetchSourcesAtom } from "~/atoms"
|
||||||
|
|
||||||
function ThemeToggle() {
|
function ThemeToggle() {
|
||||||
const { toggleDark } = useDark()
|
const { toggleDark } = useDark()
|
||||||
@ -19,7 +19,7 @@ function ThemeToggle() {
|
|||||||
|
|
||||||
function RefreshButton() {
|
function RefreshButton() {
|
||||||
const currentSection = useAtomValue(currentSectionAtom)
|
const currentSection = useAtomValue(currentSectionAtom)
|
||||||
const setRefetchSource = useSetAtom(refetchSourceAtom)
|
const setRefetchSource = useSetAtom(refetchSourcesAtom)
|
||||||
const refreshAll = useCallback(() => {
|
const refreshAll = useCallback(() => {
|
||||||
const obj = Object.fromEntries(currentSection.sourceList.map(id => [id, Date.now()]))
|
const obj = Object.fromEntries(currentSection.sourceList.map(id => [id, Date.now()]))
|
||||||
setRefetchSource(prev => ({
|
setRefetchSource(prev => ({
|
||||||
@ -43,7 +43,6 @@ export function Header() {
|
|||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<RefreshButton />
|
<RefreshButton />
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
<Link className="i-ph:gear btn-pure" to="/setting" />
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
|
49
src/components/header.tsx
Normal file
49
src/components/header.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
@ -8,7 +8,7 @@ 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 { focusSourcesAtom, refetchSourceAtom } from "~/atoms"
|
import { focusSourcesAtom, refetchSourcesAtom } from "~/atoms"
|
||||||
import { useRelativeTime } from "~/hooks/useRelativeTime"
|
import { useRelativeTime } from "~/hooks/useRelativeTime"
|
||||||
|
|
||||||
export interface ItemsProps extends React.HTMLAttributes<HTMLDivElement> {
|
export interface ItemsProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
@ -62,7 +62,7 @@ export const CardWrapper = forwardRef<HTMLDivElement, ItemsProps>(({ id, isDragg
|
|||||||
|
|
||||||
export function NewsCard({ id, inView, isOverlay, handleListeners }: NewsCardProps) {
|
export function NewsCard({ id, inView, isOverlay, handleListeners }: NewsCardProps) {
|
||||||
const [focusSources, setFocusSources] = useAtom(focusSourcesAtom)
|
const [focusSources, setFocusSources] = useAtom(focusSourcesAtom)
|
||||||
const [refetchSource, setRefetchSource] = useAtom(refetchSourceAtom)
|
const [refetchSource, setRefetchSource] = useAtom(refetchSourcesAtom)
|
||||||
const query = useQuery({
|
const query = useQuery({
|
||||||
queryKey: [id, refetchSource[id]],
|
queryKey: [id, refetchSource[id]],
|
||||||
queryFn: async ({ queryKey }) => {
|
queryFn: async ({ queryKey }) => {
|
||||||
|
@ -13,9 +13,8 @@ import { SortableContext, arrayMove, rectSortingStrategy, useSortable } from "@d
|
|||||||
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 { CSS } from "@dnd-kit/utilities"
|
||||||
import { GridContainer } from "./Pure"
|
import type { ItemsProps } from "./card"
|
||||||
import type { ItemsProps } from "./Card"
|
import { CardWrapper } from "./card"
|
||||||
import { CardWrapper } from "./Card"
|
|
||||||
import { focusSourcesAtom } from "~/atoms"
|
import { focusSourcesAtom } from "~/atoms"
|
||||||
|
|
||||||
export function Dnd() {
|
export function Dnd() {
|
||||||
@ -53,11 +52,9 @@ export function Dnd() {
|
|||||||
onDragCancel={handleDragCancel}
|
onDragCancel={handleDragCancel}
|
||||||
>
|
>
|
||||||
<SortableContext items={items} strategy={rectSortingStrategy}>
|
<SortableContext items={items} strategy={rectSortingStrategy}>
|
||||||
<GridContainer>
|
|
||||||
{items.map(id => (
|
{items.map(id => (
|
||||||
<SortableCardWrapper key={id} id={id} />
|
<SortableCardWrapper key={id} id={id} />
|
||||||
))}
|
))}
|
||||||
</GridContainer>
|
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
<DragOverlay adjustScale style={{ transformOrigin: "0 0 " }}>
|
<DragOverlay adjustScale style={{ transformOrigin: "0 0 " }}>
|
||||||
{!!activeId && <CardWrapper id={activeId as SourceID} isOverlay />}
|
{!!activeId && <CardWrapper id={activeId as SourceID} isOverlay />}
|
||||||
|
@ -1,28 +0,0 @@
|
|||||||
import { useAtomValue } from "jotai"
|
|
||||||
import type { PropsWithChildren } from "react"
|
|
||||||
import { CardWrapper } from "./Card"
|
|
||||||
import { currentSectionAtom } from "~/atoms"
|
|
||||||
|
|
||||||
export function GridContainer({ children }: PropsWithChildren) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="grid w-full gap-5"
|
|
||||||
style={{
|
|
||||||
gridTemplateColumns: "repeat(auto-fill, minmax(350px, 1fr))",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Pure() {
|
|
||||||
const currentSection = useAtomValue(currentSectionAtom)
|
|
||||||
return (
|
|
||||||
<GridContainer>
|
|
||||||
{currentSection.sourceList.map(id => (
|
|
||||||
<CardWrapper key={id} id={id} />
|
|
||||||
))}
|
|
||||||
</GridContainer>
|
|
||||||
)
|
|
||||||
}
|
|
199
src/components/section/card.tsx
Normal file
199
src/components/section/card.tsx
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
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>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
92
src/components/section/dnd.tsx
Normal file
92
src/components/section/dnd.tsx
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
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}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
55
src/components/section/index.tsx
Normal file
55
src/components/section/index.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { metadata, sectionIds } from "@shared/data"
|
||||||
|
import type { SectionID } from "@shared/types"
|
||||||
|
import { Link } from "@tanstack/react-router"
|
||||||
|
import clsx from "clsx"
|
||||||
|
import { useSetAtom } from "jotai"
|
||||||
|
import { useEffect } from "react"
|
||||||
|
import { Dnd } from "./dnd"
|
||||||
|
import { CardWrapper } from "./card"
|
||||||
|
import { currentSectionIDAtom } from "~/atoms"
|
||||||
|
|
||||||
|
export function Section({ id }: { id: SectionID }) {
|
||||||
|
const setCurrentSectionID = useSetAtom(currentSectionIDAtom)
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentSectionID(id)
|
||||||
|
}, [id, setCurrentSectionID])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col justify-center items-center">
|
||||||
|
<section className="flex gap-2 py-4 sm:mt--12">
|
||||||
|
{sectionIds.map(section => (
|
||||||
|
<Link
|
||||||
|
key={section}
|
||||||
|
to="/s/$section"
|
||||||
|
params={{ section }}
|
||||||
|
className={clsx(
|
||||||
|
"btn-action-sm",
|
||||||
|
id === section && "btn-action-active",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{metadata[section].name}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="grid w-full gap-5"
|
||||||
|
style={{
|
||||||
|
gridTemplateColumns: "repeat(auto-fill, minmax(350px, 1fr))",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{id === "focus"
|
||||||
|
? <Dnd />
|
||||||
|
: (
|
||||||
|
<>
|
||||||
|
{
|
||||||
|
metadata[id].sourceList.map(source => (
|
||||||
|
<CardWrapper key={source} id={source} />
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -11,18 +11,18 @@
|
|||||||
// Import Routes
|
// Import Routes
|
||||||
|
|
||||||
import { Route as rootRoute } from './routes/__root'
|
import { Route as rootRoute } from './routes/__root'
|
||||||
import { Route as SettingImport } from './routes/setting'
|
|
||||||
import { Route as IndexImport } from './routes/index'
|
import { Route as IndexImport } from './routes/index'
|
||||||
|
import { Route as SSectionImport } from './routes/s.$section'
|
||||||
|
|
||||||
// Create/Update Routes
|
// Create/Update Routes
|
||||||
|
|
||||||
const SettingRoute = SettingImport.update({
|
const IndexRoute = IndexImport.update({
|
||||||
path: '/setting',
|
path: '/',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
const IndexRoute = IndexImport.update({
|
const SSectionRoute = SSectionImport.update({
|
||||||
path: '/',
|
path: '/s/$section',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
@ -37,11 +37,11 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof IndexImport
|
preLoaderRoute: typeof IndexImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
'/setting': {
|
'/s/$section': {
|
||||||
id: '/setting'
|
id: '/s/$section'
|
||||||
path: '/setting'
|
path: '/s/$section'
|
||||||
fullPath: '/setting'
|
fullPath: '/s/$section'
|
||||||
preLoaderRoute: typeof SettingImport
|
preLoaderRoute: typeof SSectionImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -51,37 +51,37 @@ declare module '@tanstack/react-router' {
|
|||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/setting': typeof SettingRoute
|
'/s/$section': typeof SSectionRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/setting': typeof SettingRoute
|
'/s/$section': typeof SSectionRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRoute
|
__root__: typeof rootRoute
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/setting': typeof SettingRoute
|
'/s/$section': typeof SSectionRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
fullPaths: '/' | '/setting'
|
fullPaths: '/' | '/s/$section'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to: '/' | '/setting'
|
to: '/' | '/s/$section'
|
||||||
id: '__root__' | '/' | '/setting'
|
id: '__root__' | '/' | '/s/$section'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
IndexRoute: typeof IndexRoute
|
IndexRoute: typeof IndexRoute
|
||||||
SettingRoute: typeof SettingRoute
|
SSectionRoute: typeof SSectionRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexRoute: IndexRoute,
|
IndexRoute: IndexRoute,
|
||||||
SettingRoute: SettingRoute,
|
SSectionRoute: SSectionRoute,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const routeTree = rootRoute
|
export const routeTree = rootRoute
|
||||||
@ -97,14 +97,14 @@ export const routeTree = rootRoute
|
|||||||
"filePath": "__root.tsx",
|
"filePath": "__root.tsx",
|
||||||
"children": [
|
"children": [
|
||||||
"/",
|
"/",
|
||||||
"/setting"
|
"/s/$section"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"/": {
|
"/": {
|
||||||
"filePath": "index.tsx"
|
"filePath": "index.tsx"
|
||||||
},
|
},
|
||||||
"/setting": {
|
"/s/$section": {
|
||||||
"filePath": "setting.tsx"
|
"filePath": "s.$section.tsx"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import "~/styles/globals.css"
|
|||||||
import "virtual:uno.css"
|
import "virtual:uno.css"
|
||||||
import type { QueryClient } from "@tanstack/react-query"
|
import type { QueryClient } from "@tanstack/react-query"
|
||||||
import { OverlayScrollbarsComponent } from "overlayscrollbars-react"
|
import { OverlayScrollbarsComponent } from "overlayscrollbars-react"
|
||||||
import { Header } from "~/components/Header"
|
import { Header } from "~/components/header"
|
||||||
import { useOnReload } from "~/hooks/useOnReload"
|
import { useOnReload } from "~/hooks/useOnReload"
|
||||||
|
|
||||||
export const Route = createRootRouteWithContext<{
|
export const Route = createRootRouteWithContext<{
|
||||||
@ -16,11 +16,11 @@ export const Route = createRootRouteWithContext<{
|
|||||||
})
|
})
|
||||||
|
|
||||||
function NotFoundComponent() {
|
function NotFoundComponent() {
|
||||||
const nav = Route.useNavigate()
|
// const nav = Route.useNavigate()
|
||||||
nav({
|
// nav({
|
||||||
to: "/",
|
// to: "/",
|
||||||
})
|
// })
|
||||||
return <div></div>
|
// return <div></div>
|
||||||
}
|
}
|
||||||
|
|
||||||
function RootComponent() {
|
function RootComponent() {
|
||||||
|
@ -1,56 +1,18 @@
|
|||||||
import { metadata, sectionIds } from "@shared/data"
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
import type { SectionID } from "@shared/types"
|
import { useAtomValue } from "jotai"
|
||||||
import { Link, createFileRoute } from "@tanstack/react-router"
|
import { useMemo } from "react"
|
||||||
import clsx from "clsx"
|
import { focusSourcesAtom } from "~/atoms"
|
||||||
import { useAtom, useAtomValue } from "jotai"
|
import { Section } from "~/components/section"
|
||||||
import { useEffect, useMemo } from "react"
|
|
||||||
import { currentSectionIDAtom, focusSourcesAtom } from "~/atoms"
|
|
||||||
import { Dnd } from "~/components/section/Dnd"
|
|
||||||
import { Pure } from "~/components/section/Pure"
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/")({
|
export const Route = createFileRoute("/")({
|
||||||
validateSearch: (search: any) => ({
|
|
||||||
section: (search.section as SectionID),
|
|
||||||
}),
|
|
||||||
component: IndexComponent,
|
component: IndexComponent,
|
||||||
})
|
})
|
||||||
|
|
||||||
function IndexComponent() {
|
function IndexComponent() {
|
||||||
const { section } = Route.useSearch()
|
|
||||||
const focusSources = useAtomValue(focusSourcesAtom)
|
const focusSources = useAtomValue(focusSourcesAtom)
|
||||||
const nav = Route.useNavigate()
|
|
||||||
const [currentSectionID, setCurrentSectionID] = useAtom(currentSectionIDAtom)
|
|
||||||
const id = useMemo(() => {
|
const id = useMemo(() => {
|
||||||
if (sectionIds.includes(section)) return section
|
return focusSources.length ? "focus" : "social"
|
||||||
else return focusSources.length ? "focus" : "social"
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [section, focusSources])
|
}, [])
|
||||||
|
return <Section id={id} />
|
||||||
useEffect(() => {
|
|
||||||
setCurrentSectionID(id)
|
|
||||||
nav({
|
|
||||||
to: "/",
|
|
||||||
search: { section: id },
|
|
||||||
replace: true,
|
|
||||||
})
|
|
||||||
}, [setCurrentSectionID, id, nav])
|
|
||||||
|
|
||||||
return currentSectionID === id && (
|
|
||||||
<div className="flex flex-col justify-center items-center">
|
|
||||||
<section className="flex gap-2 py-4">
|
|
||||||
{sectionIds.map(section => (
|
|
||||||
<Link
|
|
||||||
key={section}
|
|
||||||
to="/"
|
|
||||||
search={{ section }}
|
|
||||||
className={clsx("btn-action-sm", id === section && "btn-action-active")}
|
|
||||||
>
|
|
||||||
{metadata[section].name}
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</section>
|
|
||||||
{
|
|
||||||
id === "focus" ? <Dnd /> : <Pure />
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
28
src/routes/s.$section.tsx
Normal file
28
src/routes/s.$section.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { createFileRoute, redirect } from "@tanstack/react-router"
|
||||||
|
import { sectionIds } from "@shared/data"
|
||||||
|
import { Section } from "~/components/section"
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/s/$section")({
|
||||||
|
component: SectionComponent,
|
||||||
|
params: {
|
||||||
|
parse: (params) => {
|
||||||
|
const section = sectionIds.find(x => x === params.section.toLowerCase())
|
||||||
|
if (!section)
|
||||||
|
throw new Error(`"${params.section}" is not a valid section.`)
|
||||||
|
return {
|
||||||
|
section,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
stringify: params => params,
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
if (error?.routerCode === "PARSE_PARAMS") {
|
||||||
|
throw redirect({ to: "/" })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
function SectionComponent() {
|
||||||
|
const { section } = Route.useParams()
|
||||||
|
return <Section id={section} />
|
||||||
|
}
|
@ -1,13 +0,0 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router"
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/setting")({
|
|
||||||
component: SettingComponent,
|
|
||||||
})
|
|
||||||
|
|
||||||
function SettingComponent() {
|
|
||||||
return (
|
|
||||||
<div className="p-2">
|
|
||||||
<h3>Setting</h3>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user