80 lines
2.3 KiB
TypeScript
80 lines
2.3 KiB
TypeScript
import { Link } from "@/i18n/navigation";
|
||
import { getLocale, getTranslations } from "next-intl/server";
|
||
import { CalendarDays, Clock } from "lucide-react";
|
||
import { Badge } from "@/components/ui/badge";
|
||
import { formatDate, toDateTimeAttribute } from "@/lib/utils";
|
||
import type { PostMeta } from "@/lib/posts/types";
|
||
|
||
/**
|
||
* 文章卡片(服务端组件)。
|
||
*
|
||
* DESIGN.md 的 card-base:白底 + 1px hairline 描边 + rounded-lg,
|
||
* 内部留白 {spacing.xl}。展示标题、日期、阅读时长、摘要与标签。
|
||
*/
|
||
export interface PostCardProps {
|
||
post: PostMeta;
|
||
/** 是否展示摘要;紧凑列表可关闭 */
|
||
showSummary?: boolean;
|
||
className?: string;
|
||
}
|
||
|
||
export async function PostCard({
|
||
post,
|
||
showSummary = true,
|
||
className,
|
||
}: PostCardProps) {
|
||
const t = await getTranslations("Post");
|
||
const locale = await getLocale();
|
||
|
||
return (
|
||
<article className={className}>
|
||
<Link
|
||
href={`/posts/${post.slug}`}
|
||
className="group flex h-full flex-col gap-sm rounded-lg border border-hairline bg-canvas p-xl transition-colors duration-150 hover:border-hairline-soft hover:bg-surface-soft active:bg-surface"
|
||
>
|
||
{/* 元信息:日期 + 阅读时长 */}
|
||
<div className="flex flex-wrap items-center gap-sm text-caption text-steel">
|
||
<span className="inline-flex items-center gap-[6px]">
|
||
<CalendarDays className="size-3.5" aria-hidden="true" />
|
||
<time dateTime={toDateTimeAttribute(post.date)}>
|
||
{formatDate(post.date, locale)}
|
||
</time>
|
||
</span>
|
||
|
||
<span aria-hidden="true" className="text-muted">
|
||
·
|
||
</span>
|
||
|
||
<span className="inline-flex items-center gap-[6px]">
|
||
<Clock className="size-3.5" aria-hidden="true" />
|
||
{t("readingTime", { minutes: post.readingMinutes })}
|
||
</span>
|
||
</div>
|
||
|
||
{/* 标题 */}
|
||
<h3 className="text-heading-4 font-semibold text-ink transition-colors duration-150 group-hover:text-brand-green-deep">
|
||
{post.title}
|
||
</h3>
|
||
|
||
{/* 摘要 */}
|
||
{showSummary ? (
|
||
<p className="flex-1 text-body-sm text-steel text-balance-pretty">
|
||
{post.summary}
|
||
</p>
|
||
) : null}
|
||
|
||
{/* 标签 */}
|
||
{post.tags && post.tags.length > 0 ? (
|
||
<ul className="mt-xs flex flex-wrap gap-xs">
|
||
{post.tags.map((tag) => (
|
||
<li key={tag}>
|
||
<Badge variant="tag">{tag}</Badge>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
) : null}
|
||
</Link>
|
||
</article>
|
||
);
|
||
}
|