frontend: add bookmark management and homepage navigation

Admin-only /bookmarks page for managing entries; homepage now renders
public bookmarks as a category-grouped navigation grid (empty state
links admin to the manager). Dashboard gains a recent-bookmarks card,
dock and main layout get a bookmark entry for admins, and the
middleware protects /bookmarks.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-03 01:51:55 +08:00
parent ffecc9451d
commit 37cecaa1ce
8 changed files with 625 additions and 42 deletions

View File

@@ -11,15 +11,31 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import { LoginForm } from "./login/login-form";
import { Card, CardContent } from "@/components/ui/card";
import { Globe, ExternalLink, Link2 } from "lucide-react";
import Link from "next/link";
interface Bookmark {
id: number;
title: string;
url: string;
description: string;
icon: string;
category: string;
}
export function HomePageClient({
isAuthenticated,
isAdmin,
healthText,
hasKeycloak,
bookmarks,
}: {
isAuthenticated: boolean;
isAdmin: boolean;
healthText: string;
hasKeycloak: boolean;
bookmarks: Bookmark[];
}) {
const searchParams = useSearchParams();
const [loginOpen, setLoginOpen] = useState(false);
@@ -30,33 +46,134 @@ export function HomePageClient({
}
}, [searchParams, isAuthenticated]);
const grouped = bookmarks.reduce<Record<string, Bookmark[]>>((acc, bm) => {
const cat = bm.category || "默认";
if (!acc[cat]) acc[cat] = [];
acc[cat].push(bm);
return acc;
}, {});
return (
<div className="relative flex min-h-screen flex-col items-center justify-center bg-gradient-to-br from-slate-50 to-slate-100 p-4 pb-24">
<BlurFade inView delay={0.1}>
<h1 className="mb-4 text-center text-4xl font-extrabold tracking-tight text-slate-900">
EvanPage
</h1>
</BlurFade>
<div className="relative flex min-h-screen flex-col items-center bg-gradient-to-br from-slate-50 to-slate-100 p-4 pb-32">
<div className="flex flex-1 flex-col items-center justify-center py-12">
<BlurFade inView delay={0.1}>
<h1 className="mb-4 text-center text-4xl font-extrabold tracking-tight text-slate-900">
EvanPage
</h1>
</BlurFade>
<BlurFade inView delay={0.2}>
<p className="mb-8 max-w-md text-center text-lg text-slate-600">
</p>
</BlurFade>
<BlurFade inView delay={0.2}>
<p className="mb-8 max-w-md text-center text-lg text-slate-600">
</p>
</BlurFade>
<BlurFade inView delay={0.3}>
<div className="rounded-2xl border bg-white/80 px-8 py-6 shadow-lg backdrop-blur">
<p className="text-center text-sm font-medium text-slate-500">
</p>
<p className="mt-2 text-center text-xl font-semibold text-slate-800">
{healthText}
</p>
<BlurFade inView delay={0.3}>
<div className="rounded-2xl border bg-white/80 px-8 py-6 shadow-lg backdrop-blur">
<p className="text-center text-sm font-medium text-slate-500">
</p>
<p className="mt-2 text-center text-xl font-semibold text-slate-800">
{healthText}
</p>
</div>
</BlurFade>
</div>
{bookmarks.length > 0 && (
<div className="w-full max-w-4xl space-y-6">
<BlurFade inView delay={0.4}>
<div className="flex items-center justify-between px-1">
<h2 className="text-lg font-semibold text-slate-800">
</h2>
{isAdmin && (
<Link
href="/bookmarks"
className="text-sm text-slate-500 hover:text-slate-800 hover:underline"
>
</Link>
)}
</div>
</BlurFade>
{Object.entries(grouped).map(([cat, items], catIdx) => (
<BlurFade key={cat} inView delay={0.4 + catIdx * 0.1}>
<div>
<h3 className="mb-3 px-1 text-xs font-semibold uppercase tracking-wide text-slate-400">
{cat}
</h3>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{items.map((bm) => (
<Card
key={bm.id}
className="group transition-shadow hover:shadow-md"
>
<CardContent className="p-0">
<a
href={bm.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-3 p-4"
>
<div className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-slate-100">
{bm.icon ? (
<img
src={bm.icon}
alt=""
className="size-5 object-contain"
onError={(e) => {
(e.target as HTMLImageElement).style.display =
"none";
}}
/>
) : (
<Globe className="size-5 text-slate-400" />
)}
</div>
<div className="min-w-0 flex-1">
<p className="truncate font-medium text-slate-800">
{bm.title}
</p>
{bm.description && (
<p className="mt-0.5 line-clamp-1 text-xs text-slate-400">
{bm.description}
</p>
)}
</div>
<ExternalLink className="size-4 shrink-0 text-slate-300 transition-colors group-hover:text-slate-500" />
</a>
</CardContent>
</Card>
))}
</div>
</div>
</BlurFade>
))}
</div>
</BlurFade>
)}
{bookmarks.length === 0 && (
<BlurFade inView delay={0.4}>
<div className="w-full max-w-md rounded-2xl border border-dashed bg-white/60 py-10 text-center backdrop-blur">
<Link2 className="mx-auto mb-3 size-8 text-slate-300" />
<p className="mb-2 text-slate-500"></p>
{isAdmin && (
<Link
href="/bookmarks"
className="text-sm font-medium text-slate-800 hover:underline"
>
</Link>
)}
</div>
</BlurFade>
)}
<HomeDock
isAuthenticated={isAuthenticated}
isAdmin={isAdmin}
onLoginClick={() => !isAuthenticated && setLoginOpen(true)}
/>