import Link from "next/link";
import { ChevronRight, Clock3 } from "lucide-react";
import { Badge } from "@/components/ui/Badge";
import { StatusDot } from "@/components/ui/StatusDot";
import type { ProjectResponse } from "@/lib/api/types";
import {
formatProjectUpdatedAt,
pendingReviewCount,
} from "@/lib/projects/projects";
import { cardClass, cn, focusRing } from "@/lib/ui/variants";
interface ProjectCardProps {
project: ProjectResponse;
compact?: boolean;
}
// 缺一句话故事时的中性占位——避免卡片正文塌陷成空白(P3-2)。
const LOGLINE_PLACEHOLDER = "还没有一句话故事,进入写作台补上一句梗概。";
// 书脊/封面暖色板:全部取自现有语义 token(朱砂/琥珀),仅调透明度,无硬编码 hex。
const COVER_TINTS = [
"bg-[var(--color-cinnabar-wash)] text-cinnabar",
"bg-cinnabar/10 text-cinnabar",
"bg-cinnabar/[0.16] text-cinnabar",
"bg-overdue/10 text-overdue",
"bg-overdue/[0.18] text-overdue",
"bg-surface-strong text-body-strong",
] as const;
// 由 id/title 确定性取暖色底色块(djb2 变体哈希,同一本书永远同一色)。
function coverTint(seed: string): string {
let hash = 0;
for (const ch of seed) hash = (hash * 31 + (ch.codePointAt(0) ?? 0)) >>> 0;
return COVER_TINTS[hash % COVER_TINTS.length] ?? COVER_TINTS[0];
}
// 取书名首个字符作封面字(Array.from 保证 CJK/emoji 不被截断)。
function coverGlyph(title: string): string {
return Array.from(title.trim())[0] ?? "书";
}
interface CoverProps {
project: ProjectResponse;
size: "sm" | "md";
}
// 书脊色块:填补此前的空白,给每本书一个稳定的视觉锚点。
function Cover({ project, size }: CoverProps) {
const tint = coverTint(project.id || project.title || "");
const box = size === "md" ? "h-12 w-12 text-title-lg" : "h-10 w-10 text-title-md";
return (
);
}
// 作品卡(UX §6.1):书脊色块 + 书名衬线 + 一句话故事 + 元信息行。
// M1 无字数/章数端点 → 不展示该统计(避免编造 API)。
export function ProjectCard({ project, compact = false }: ProjectCardProps) {
const pendingCount = pendingReviewCount(project);
const updatedLabel = formatProjectUpdatedAt(project.updated_at);
const title = project.title || "未命名作品";
const logline = project.logline?.trim();
if (compact) {
return (
{logline ?? LOGLINE_PLACEHOLDER}