mirror of
https://github.com/ourongxing/newsnow.git
synced 2025-01-19 03:09: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)
|
||||
})
|
||||
|
||||
function initRefetchSource() {
|
||||
function initRefetchSources() {
|
||||
let time = 0
|
||||
// useOnReload
|
||||
// 没有放在 useOnReload 里面, 可以避免初始化后再修改 refetchSourceAtom,导致多次请求 API
|
||||
@ -20,7 +20,7 @@ function initRefetchSource() {
|
||||
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")
|
||||
|
||||
|
@ -3,7 +3,7 @@ import { useCallback } from "react"
|
||||
import { useAtomValue, useSetAtom } from "jotai"
|
||||
import logo from "~/assets/react.svg"
|
||||
import { useDark } from "~/hooks/useDark"
|
||||
import { currentSectionAtom, refetchSourceAtom } from "~/atoms"
|
||||
import { currentSectionAtom, refetchSourcesAtom } from "~/atoms"
|
||||
|
||||
function ThemeToggle() {
|
||||
const { toggleDark } = useDark()
|
||||
@ -19,7 +19,7 @@ function ThemeToggle() {
|
||||
|
||||
function RefreshButton() {
|
||||
const currentSection = useAtomValue(currentSectionAtom)
|
||||
const setRefetchSource = useSetAtom(refetchSourceAtom)
|
||||
const setRefetchSource = useSetAtom(refetchSourcesAtom)
|
||||
const refreshAll = useCallback(() => {
|
||||
const obj = Object.fromEntries(currentSection.sourceList.map(id => [id, Date.now()]))
|
||||
setRefetchSource(prev => ({
|
||||
@ -43,7 +43,6 @@ export function Header() {
|
||||
<div className="flex gap-2">
|
||||
<RefreshButton />
|
||||
<ThemeToggle />
|
||||
<Link className="i-ph:gear btn-pure" to="/setting" />
|
||||
</div>
|
||||
</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 { sources } from "@shared/data"
|
||||
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"
|
||||
|
||||
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) {
|
||||
const [focusSources, setFocusSources] = useAtom(focusSourcesAtom)
|
||||
const [refetchSource, setRefetchSource] = useAtom(refetchSourceAtom)
|
||||
const [refetchSource, setRefetchSource] = useAtom(refetchSourcesAtom)
|
||||
const query = useQuery({
|
||||
queryKey: [id, refetchSource[id]],
|
||||
queryFn: async ({ queryKey }) => {
|
||||
|
@ -13,9 +13,8 @@ import { SortableContext, arrayMove, rectSortingStrategy, useSortable } from "@d
|
||||
import { useAtom } from "jotai"
|
||||
import type { SourceID } from "@shared/types"
|
||||
import { CSS } from "@dnd-kit/utilities"
|
||||
import { GridContainer } from "./Pure"
|
||||
import type { ItemsProps } from "./Card"
|
||||
import { CardWrapper } from "./Card"
|
||||
import type { ItemsProps } from "./card"
|
||||
import { CardWrapper } from "./card"
|
||||
import { focusSourcesAtom } from "~/atoms"
|
||||
|
||||
export function Dnd() {
|
||||
@ -53,11 +52,9 @@ export function Dnd() {
|
||||
onDragCancel={handleDragCancel}
|
||||
>
|
||||
<SortableContext items={items} strategy={rectSortingStrategy}>
|
||||
<GridContainer>
|
||||
{items.map(id => (
|
||||
<SortableCardWrapper key={id} id={id} />
|
||||
))}
|
||||
</GridContainer>
|
||||
{items.map(id => (
|
||||
<SortableCardWrapper key={id} id={id} />
|
||||
))}
|
||||
</SortableContext>
|
||||
<DragOverlay adjustScale style={{ transformOrigin: "0 0 " }}>
|
||||
{!!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 { Route as rootRoute } from './routes/__root'
|
||||
import { Route as SettingImport } from './routes/setting'
|
||||
import { Route as IndexImport } from './routes/index'
|
||||
import { Route as SSectionImport } from './routes/s.$section'
|
||||
|
||||
// Create/Update Routes
|
||||
|
||||
const SettingRoute = SettingImport.update({
|
||||
path: '/setting',
|
||||
const IndexRoute = IndexImport.update({
|
||||
path: '/',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const IndexRoute = IndexImport.update({
|
||||
path: '/',
|
||||
const SSectionRoute = SSectionImport.update({
|
||||
path: '/s/$section',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
@ -37,11 +37,11 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof IndexImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/setting': {
|
||||
id: '/setting'
|
||||
path: '/setting'
|
||||
fullPath: '/setting'
|
||||
preLoaderRoute: typeof SettingImport
|
||||
'/s/$section': {
|
||||
id: '/s/$section'
|
||||
path: '/s/$section'
|
||||
fullPath: '/s/$section'
|
||||
preLoaderRoute: typeof SSectionImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
}
|
||||
@ -51,37 +51,37 @@ declare module '@tanstack/react-router' {
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/setting': typeof SettingRoute
|
||||
'/s/$section': typeof SSectionRoute
|
||||
}
|
||||
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/setting': typeof SettingRoute
|
||||
'/s/$section': typeof SSectionRoute
|
||||
}
|
||||
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRoute
|
||||
'/': typeof IndexRoute
|
||||
'/setting': typeof SettingRoute
|
||||
'/s/$section': typeof SSectionRoute
|
||||
}
|
||||
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths: '/' | '/setting'
|
||||
fullPaths: '/' | '/s/$section'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/' | '/setting'
|
||||
id: '__root__' | '/' | '/setting'
|
||||
to: '/' | '/s/$section'
|
||||
id: '__root__' | '/' | '/s/$section'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
SettingRoute: typeof SettingRoute
|
||||
SSectionRoute: typeof SSectionRoute
|
||||
}
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
SettingRoute: SettingRoute,
|
||||
SSectionRoute: SSectionRoute,
|
||||
}
|
||||
|
||||
export const routeTree = rootRoute
|
||||
@ -97,14 +97,14 @@ export const routeTree = rootRoute
|
||||
"filePath": "__root.tsx",
|
||||
"children": [
|
||||
"/",
|
||||
"/setting"
|
||||
"/s/$section"
|
||||
]
|
||||
},
|
||||
"/": {
|
||||
"filePath": "index.tsx"
|
||||
},
|
||||
"/setting": {
|
||||
"filePath": "setting.tsx"
|
||||
"/s/$section": {
|
||||
"filePath": "s.$section.tsx"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import "~/styles/globals.css"
|
||||
import "virtual:uno.css"
|
||||
import type { QueryClient } from "@tanstack/react-query"
|
||||
import { OverlayScrollbarsComponent } from "overlayscrollbars-react"
|
||||
import { Header } from "~/components/Header"
|
||||
import { Header } from "~/components/header"
|
||||
import { useOnReload } from "~/hooks/useOnReload"
|
||||
|
||||
export const Route = createRootRouteWithContext<{
|
||||
@ -16,11 +16,11 @@ export const Route = createRootRouteWithContext<{
|
||||
})
|
||||
|
||||
function NotFoundComponent() {
|
||||
const nav = Route.useNavigate()
|
||||
nav({
|
||||
to: "/",
|
||||
})
|
||||
return <div></div>
|
||||
// const nav = Route.useNavigate()
|
||||
// nav({
|
||||
// to: "/",
|
||||
// })
|
||||
// return <div></div>
|
||||
}
|
||||
|
||||
function RootComponent() {
|
||||
|
@ -1,56 +1,18 @@
|
||||
import { metadata, sectionIds } from "@shared/data"
|
||||
import type { SectionID } from "@shared/types"
|
||||
import { Link, createFileRoute } from "@tanstack/react-router"
|
||||
import clsx from "clsx"
|
||||
import { useAtom, useAtomValue } from "jotai"
|
||||
import { useEffect, useMemo } from "react"
|
||||
import { currentSectionIDAtom, focusSourcesAtom } from "~/atoms"
|
||||
import { Dnd } from "~/components/section/Dnd"
|
||||
import { Pure } from "~/components/section/Pure"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { useAtomValue } from "jotai"
|
||||
import { useMemo } from "react"
|
||||
import { focusSourcesAtom } from "~/atoms"
|
||||
import { Section } from "~/components/section"
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
validateSearch: (search: any) => ({
|
||||
section: (search.section as SectionID),
|
||||
}),
|
||||
component: IndexComponent,
|
||||
})
|
||||
|
||||
function IndexComponent() {
|
||||
const { section } = Route.useSearch()
|
||||
const focusSources = useAtomValue(focusSourcesAtom)
|
||||
const nav = Route.useNavigate()
|
||||
const [currentSectionID, setCurrentSectionID] = useAtom(currentSectionIDAtom)
|
||||
const id = useMemo(() => {
|
||||
if (sectionIds.includes(section)) return section
|
||||
else return focusSources.length ? "focus" : "social"
|
||||
}, [section, focusSources])
|
||||
|
||||
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>
|
||||
)
|
||||
return focusSources.length ? "focus" : "social"
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
return <Section id={id} />
|
||||
}
|
||||
|
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