138 lines
6.0 KiB
TypeScript
138 lines
6.0 KiB
TypeScript
import type { QueryClient } from '@tanstack/react-query'
|
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
|
import { createFileRoute, Link } from '@tanstack/react-router'
|
|
import * as icons from 'lucide-react'
|
|
import { ArrowRight, Compass, Plus } from 'lucide-react'
|
|
import * as motion from 'motion/react-client'
|
|
import { orpc } from '@/client/orpc'
|
|
|
|
const allIcons = icons as unknown as Record<string, React.ComponentType<{ className?: string }>>
|
|
|
|
export const Route = createFileRoute('/_protected/' as never)({
|
|
loader: async ({ context }: { context: { queryClient: QueryClient } }) => {
|
|
await context.queryClient.fetchQuery(orpc.bookmarks.category.list.queryOptions())
|
|
},
|
|
component: DashboardPage,
|
|
})
|
|
|
|
const getGreeting = (hour: number): string => {
|
|
if (hour >= 5 && hour < 12) return '早上好'
|
|
if (hour >= 12 && hour < 14) return '中午好'
|
|
if (hour >= 14 && hour < 18) return '下午好'
|
|
return '晚上好'
|
|
}
|
|
|
|
const formatDate = (date: Date): string => {
|
|
const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
|
|
const year = date.getFullYear()
|
|
const month = date.getMonth() + 1
|
|
const day = date.getDate()
|
|
const weekday = weekdays[date.getDay()]
|
|
return `${year}年${month}月${day}日 ${weekday}`
|
|
}
|
|
|
|
const containerVariants = {
|
|
hidden: { opacity: 0 },
|
|
visible: {
|
|
opacity: 1,
|
|
transition: { staggerChildren: 0.06, delayChildren: 0.1 },
|
|
},
|
|
}
|
|
|
|
const itemVariants = {
|
|
hidden: { opacity: 0, y: 12 },
|
|
visible: { opacity: 1, y: 0, transition: { duration: 0.4, ease: [0.25, 0.46, 0.45, 0.94] as const } },
|
|
}
|
|
|
|
function DashboardPage() {
|
|
const { data: categories } = useSuspenseQuery(orpc.bookmarks.category.list.queryOptions())
|
|
const now = new Date()
|
|
|
|
const totalBookmarks = categories.reduce(
|
|
(sum: number, cat: { bookmarks: Array<{ id: string }> }) => sum + cat.bookmarks.length,
|
|
0,
|
|
)
|
|
const topBookmarks = categories
|
|
.flatMap((cat: { bookmarks: Array<{ id: string; name: string; url: string; icon: string | null }> }) =>
|
|
cat.bookmarks.slice(0, 4),
|
|
)
|
|
.slice(0, 8)
|
|
|
|
return (
|
|
<motion.div className="flex-1 px-6 pb-8" variants={containerVariants} initial="hidden" animate="visible">
|
|
<motion.div variants={itemVariants} className="mb-8">
|
|
<h1 className="text-3xl font-bold tracking-tight">{getGreeting(now.getHours())}</h1>
|
|
<p className="mt-1 text-muted-foreground">{formatDate(now)}</p>
|
|
</motion.div>
|
|
|
|
{topBookmarks.length > 0 && (
|
|
<motion.div variants={itemVariants} className="mb-8">
|
|
<div className="mb-4 flex items-center justify-between">
|
|
<h2 className="text-sm font-medium text-muted-foreground">常用书签</h2>
|
|
<Link
|
|
to={'/bookmarks' as never}
|
|
className="inline-flex items-center gap-1 text-xs text-muted-foreground transition-colors hover:text-foreground"
|
|
>
|
|
查看全部
|
|
<ArrowRight className="size-3" />
|
|
</Link>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6">
|
|
{topBookmarks.map((bookmark: { id: string; name: string; url: string; icon: string | null }) => {
|
|
const Icon = (bookmark.icon && allIcons[bookmark.icon]) || icons.Globe
|
|
return (
|
|
<a
|
|
key={bookmark.id}
|
|
href={bookmark.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="group flex items-center gap-3 rounded-xl border bg-card px-3.5 py-3 transition-all duration-200 hover:-translate-y-0.5 hover:border-foreground/10 hover:shadow-md"
|
|
>
|
|
<div className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted/60 transition-colors group-hover:bg-muted">
|
|
<Icon className="size-4 text-muted-foreground transition-colors group-hover:text-foreground" />
|
|
</div>
|
|
<span className="min-w-0 truncate text-sm font-medium">{bookmark.name}</span>
|
|
</a>
|
|
)
|
|
})}
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
|
|
<motion.div variants={itemVariants}>
|
|
<h2 className="mb-4 text-sm font-medium text-muted-foreground">概览</h2>
|
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
<Link
|
|
to={'/bookmarks' as never}
|
|
className="group flex items-center gap-4 rounded-xl border bg-card p-5 transition-all duration-200 hover:-translate-y-0.5 hover:border-foreground/10 hover:shadow-md"
|
|
>
|
|
<div className="flex size-10 shrink-0 items-center justify-center rounded-xl bg-muted/60 transition-colors group-hover:bg-muted">
|
|
<Compass className="size-5 text-muted-foreground transition-colors group-hover:text-foreground" />
|
|
</div>
|
|
<div className="min-w-0 flex-1">
|
|
<p className="text-sm font-medium">书签导航</p>
|
|
<p className="text-xs text-muted-foreground">
|
|
{categories.length} 个分类 · {totalBookmarks} 个书签
|
|
</p>
|
|
</div>
|
|
<ArrowRight className="size-4 text-muted-foreground opacity-0 transition-all group-hover:opacity-100" />
|
|
</Link>
|
|
|
|
<Link
|
|
to={'/bookmarks' as never}
|
|
className="group flex items-center gap-4 rounded-xl border border-dashed bg-card p-5 transition-all duration-200 hover:-translate-y-0.5 hover:border-foreground/10 hover:shadow-md"
|
|
>
|
|
<div className="flex size-10 shrink-0 items-center justify-center rounded-xl bg-muted/60 transition-colors group-hover:bg-muted">
|
|
<Plus className="size-5 text-muted-foreground transition-colors group-hover:text-foreground" />
|
|
</div>
|
|
<div className="min-w-0 flex-1">
|
|
<p className="text-sm font-medium">添加书签</p>
|
|
<p className="text-xs text-muted-foreground">快速添加常用链接</p>
|
|
</div>
|
|
</Link>
|
|
</div>
|
|
</motion.div>
|
|
</motion.div>
|
|
)
|
|
}
|