Files
kairos/apps/server/src/routes/_protected/index.tsx
T

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>
)
}