Skip to content

Commit be3fd77

Browse files
committed
feat(profile): 个人主页 /u/[username] SSR + Bento 布局
MVP 范围(UX + PM 对齐后砍刀): - 路由:/u/[username] SSR(ISR 300s) - 数据源:后端 GET /api/user-center/profile/{username} - 布局:12-col bento,左 col-span-5 Identity,右 col-span-7 小卡网格 - 间距:gap-8 统一,用户明确要求松散 - 三种卡片:PROJ / PAPER / DOC,前两者读 preferences,DOC 读 leaderboard - hover 展开用 max-height 原地展开;mobile 改 tap toggle 砍掉(V2): - sticky 左大块(滚动竞争) - 绝对定位浮层(移动端 mess) - 编辑页(复用后端 PATCH /preferences) - Zotero 实时关联(pinned_papers 直接存 title/author/year) 配套后端改动见 involutionhell-backend main commit 8efae54 (新增 GET /api/user-center/profile/{username} + SaToken 白名单) 同步新增: - docs/architecture/frontend-backend-separation.md 固化前后端分离约定
1 parent d0a420d commit be3fd77

3 files changed

Lines changed: 587 additions & 0 deletions

File tree

app/u/[username]/ProfileCard.tsx

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import Link from "next/link";
5+
6+
interface ProfileCardProps {
7+
kind: "PROJ" | "PAPER" | "DOC";
8+
index: number;
9+
title: string;
10+
meta?: string;
11+
summary?: string;
12+
/** 展开态显示的详细内容(hover / tap 触发),没有则复用 summary */
13+
detail?: string;
14+
href?: string;
15+
/** DOC 汇总卡占两列;其他卡片单列 */
16+
spanFull?: boolean;
17+
}
18+
19+
/**
20+
* 个人主页小卡:静态展示 SEC 编号 + 标题 + meta + 概述;
21+
* 悬停(desktop)或点击(mobile)展开详情段落,用 max-height 原地展开避免布局抖动。
22+
*
23+
* 设计刻意用 editorial 硬朗风格:无圆角、1px border、hover 加硬阴影,
24+
* 和站内 Classified Archives / Top Rank 小卡统一。
25+
*/
26+
export function ProfileCard({
27+
kind,
28+
index,
29+
title,
30+
meta,
31+
summary,
32+
detail,
33+
href,
34+
spanFull,
35+
}: ProfileCardProps) {
36+
const [expanded, setExpanded] = useState(false);
37+
38+
const kindLabel = {
39+
PROJ: "Project",
40+
PAPER: "Paper",
41+
DOC: "Docs",
42+
}[kind];
43+
44+
return (
45+
<article
46+
// onClick 提供 mobile tap toggle;desktop 的 hover 通过 group-hover 样式实现
47+
onClick={() => setExpanded((v) => !v)}
48+
className={[
49+
"group relative border border-[var(--foreground)] bg-[var(--background)]",
50+
"p-6 flex flex-col gap-3 min-h-[180px] cursor-pointer",
51+
"transition-shadow duration-200 ease-out",
52+
"hover:shadow-[6px_6px_0_var(--foreground)]",
53+
spanFull ? "sm:col-span-2" : "",
54+
].join(" ")}
55+
>
56+
<div className="flex items-baseline justify-between gap-2">
57+
<span className="font-mono text-[10px] uppercase tracking-widest text-neutral-500">
58+
SEC. {kind} · {String(index).padStart(3, "0")}
59+
</span>
60+
<span className="font-mono text-[9px] uppercase tracking-widest text-neutral-400">
61+
{kindLabel}
62+
</span>
63+
</div>
64+
65+
<h3 className="font-serif text-lg md:text-xl font-bold leading-tight text-[var(--foreground)] line-clamp-2">
66+
{title}
67+
</h3>
68+
69+
{meta && (
70+
<p className="font-mono text-[10px] uppercase tracking-wider text-neutral-500 line-clamp-1">
71+
{meta}
72+
</p>
73+
)}
74+
75+
{summary && (
76+
<p className="text-[13px] leading-relaxed text-neutral-700 dark:text-neutral-300 line-clamp-2">
77+
{summary}
78+
</p>
79+
)}
80+
81+
{/* 展开态:hover(desktop)或点击(mobile)打开;用 max-height 过渡 */}
82+
{detail && (
83+
<div
84+
className={[
85+
"overflow-hidden transition-[max-height,opacity] duration-300 ease-out",
86+
"lg:group-hover:max-h-80 lg:group-hover:opacity-100",
87+
"lg:group-focus-within:max-h-80 lg:group-focus-within:opacity-100",
88+
expanded
89+
? "max-h-80 opacity-100 mt-2 pt-3 border-t border-[var(--foreground)]"
90+
: "max-h-0 opacity-0 lg:group-hover:mt-2 lg:group-hover:pt-3 lg:group-hover:border-t lg:group-hover:border-[var(--foreground)]",
91+
].join(" ")}
92+
>
93+
<p className="text-[13px] leading-relaxed text-neutral-700 dark:text-neutral-300 whitespace-pre-wrap">
94+
{detail}
95+
</p>
96+
</div>
97+
)}
98+
99+
{href && (
100+
<div className="mt-auto pt-3">
101+
<Link
102+
href={href}
103+
target={href.startsWith("http") ? "_blank" : undefined}
104+
rel={href.startsWith("http") ? "noopener noreferrer" : undefined}
105+
onClick={(e) => e.stopPropagation()}
106+
className="font-mono text-[10px] uppercase tracking-widest text-[#CC0000] hover:underline"
107+
>
108+
View →
109+
</Link>
110+
</div>
111+
)}
112+
</article>
113+
);
114+
}

0 commit comments

Comments
 (0)