Files
jiang13-bbs/frontend/components/home/CommunityPanels.tsx

355 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Link from "next/link";
import {
Flame,
Users,
MessageCircle,
Heart,
UserPlus,
Link2,
Megaphone,
BarChart3,
PenLine,
Activity,
TrendingUp,
} from "lucide-react";
import type {
PostListItem,
ActiveUser,
OverviewStats,
AnnouncementSummary,
NewUser,
CheckinStatus,
} from "@/lib/api";
import Avatar from "@/components/Avatar";
import { PulseCheckin } from "./CheckinCard";
import { FRIEND_LINKS } from "@/lib/friendLinks";
import { formatCompact, formatShortDate } from "@/lib/format";
/* ---------- 站点数据(2×2 bento 统计瓦片) ---------- */
function PulseCard({ stats, checkin }: { stats: OverviewStats; checkin?: CheckinStatus | null }) {
const cells = [
{
icon: <Users size={12.5} />,
label: "注册用户",
value: formatCompact(stats.users),
foot: `+${formatCompact(stats.today_users)} 今日`,
up: true,
},
{
icon: <PenLine size={12.5} />,
label: "话题总数",
value: formatCompact(stats.posts),
foot: `+${formatCompact(stats.today_posts)} 今日`,
up: true,
},
{
icon: <MessageCircle size={12.5} />,
label: "回复总数",
value: formatCompact(stats.comments),
foot: `+${formatCompact(stats.today_comments)} 今日`,
up: true,
},
{
icon: <Activity size={12.5} />,
label: "在线用户",
value: formatCompact(stats.online),
foot: `峰值 ${formatCompact(stats.peak_online)}`,
up: false,
},
];
return (
<section className="j13-module" aria-label="站点数据">
<div className="flex items-center justify-between">
<p className="j13-module-title">
<BarChart3 size={15} style={{ color: "var(--accent)" }} /> 站点数据
</p>
<span
className="inline-flex items-center gap-1.5 text-[11px] font-normal normal-case tracking-normal"
style={{ color: "var(--ink-3)" }}
data-tip="近 5 分钟在线"
>
<span className="j13-pulse-dot" aria-hidden="true" />
实时
</span>
</div>
<div className="j13-pulse">
{cells.map((c) => (
<div key={c.label} className="j13-pulse-cell">
<p className="j13-pulse-lab">
<span style={{ color: "var(--accent)" }} aria-hidden="true">
{c.icon}
</span>
{c.label}
</p>
<p className="j13-pulse-num">{c.value}</p>
<p className={`j13-pulse-foot${c.up ? " is-up" : ""}`}>
{c.up && <TrendingUp size={10.5} aria-hidden="true" />}
{c.foot}
</p>
</div>
))}
</div>
<div className="j13-pulse-checkin">
<PulseCheckin initial={checkin} />
</div>
</section>
);
}
/* ---------- 站点公告 ---------- */
function Announcements({ items }: { items: AnnouncementSummary[] }) {
return (
<section className="j13-module" aria-label="站点公告">
<p className="j13-module-title">
<Megaphone size={15} style={{ color: "var(--accent)" }} /> 站点公告
</p>
{items.length === 0 ? (
<p className="meta mt-2 px-0.5">暂无公告</p>
) : (
<ul className="j13-anno-list">
{items.map((a) => (
<li key={a.id}>
<Link
href={`/announcement/${a.id}`}
className="j13-anno-item"
data-tip={a.title}
>
<span className="j13-anno-item-body">
<span className="j13-anno-item-title truncate">{a.title}</span>
<span className="j13-anno-item-meta">
{a.tag ? (
<span className={`j13-anno-tag is-${a.tag_color}`}>{a.tag}</span>
) : null}
<span className="meta text-[11px] tabular-nums">
{formatShortDate(a.created_at)}
</span>
</span>
</span>
</Link>
</li>
))}
</ul>
)}
</section>
);
}
/* ---------- 热门榜 ---------- */
function HotRanking({ posts }: { posts: PostListItem[] }) {
return (
<section className="j13-module" aria-label="热门讨论">
<p className="j13-module-title mb-1.5">
<Flame size={15} style={{ color: "var(--clay)" }} /> 热门讨论
</p>
{posts.length === 0 ? (
<p className="meta px-1.5 py-3">还没有热门讨论</p>
) : (
<ol className="j13-ranklist">
{posts.map((p, i) => (
<li key={p.id}>
<Link href={`/post/${p.id}`} className="j13-rank" data-tip={p.title}>
<span className="j13-rank-no">{i + 1}</span>
<span className="min-w-0 flex-1">
<span className="j13-rank-title truncate">
{p.title}
</span>
<span className="mt-1 flex items-center gap-2.5 meta text-[11.5px]">
<span className="inline-flex items-center gap-1">
<MessageCircle size={11.5} /> {p.comment_count}
</span>
<span className="inline-flex items-center gap-1">
<Heart size={11.5} /> {p.like_count}
</span>
</span>
</span>
</Link>
</li>
))}
</ol>
)}
</section>
);
}
/* ---------- 最新注册 ---------- */
function NewUsers({ users }: { users: NewUser[] }) {
if (users.length === 0) return null;
return (
<section className="j13-module" aria-label="最新注册">
<p className="j13-module-title mb-3">
<UserPlus size={15} style={{ color: "var(--accent)" }} /> 最新注册
</p>
<ul className="grid grid-cols-4 gap-y-3">
{users.map((u) => {
const name = u.nickname || u.username;
return (
<li key={u.id}>
<Link
href={`/u/${u.id}`}
className="flex flex-col items-center gap-1.5 rounded-lg px-1 py-1 transition-colors hover:bg-[var(--accent-soft)] focus-visible:bg-[var(--accent-soft)]"
>
<Avatar name={name} src={u.avatar} size={38} />
<span
className="w-full truncate text-center text-[11px]"
style={{ color: "var(--ink-2)" }}
data-tip={name}
>
{name}
</span>
</Link>
</li>
);
})}
</ul>
</section>
);
}
/* ---------- 活跃成员(本周发帖 / 回复) ---------- */
function ActiveUsers({ users }: { users: ActiveUser[] }) {
return (
<section className="j13-module" aria-label="本周活跃成员">
<p className="j13-module-title mb-2">
<Users size={15} style={{ color: "var(--accent)" }} /> 活跃成员
<span className="ml-auto font-normal normal-case tracking-normal text-[11px]" style={{ color: "var(--ink-3)" }}>
本周
</span>
</p>
{users.length === 0 ? (
<p className="meta px-1.5 py-2">本周还没有人互动</p>
) : (
<ul className="space-y-0.5">
{users.map((u) => {
const name = u.nickname || u.username;
return (
<li key={u.id}>
<Link href={`/u/${u.id}`} className="j13-raillink" data-tip={name}>
<Avatar name={name} src={u.avatar} size={30} />
<span className="flex-1 min-w-0">
<span
className="block truncate text-[13px] font-medium leading-tight"
style={{ color: "var(--ink)" }}
>
{name}
</span>
<span className="mt-px flex items-center gap-2.5 text-[11px] meta">
<span className="inline-flex items-center gap-1">
<PenLine size={10.5} /> 发帖 {u.post_count}
</span>
<span className="inline-flex items-center gap-1">
<MessageCircle size={10.5} /> 回复 {u.comment_count}
</span>
</span>
</span>
<span
className="shrink-0 text-[13px] tabular-nums"
style={{ color: "var(--accent)" }}
aria-label={`本周共 ${u.post_count + u.comment_count} 次互动`}
>
{u.post_count + u.comment_count}
</span>
</Link>
</li>
);
})}
</ul>
)}
</section>
);
}
/* ---------- 友情链接 ---------- */
function FriendLinks() {
return (
<section className="j13-module" aria-label="友情链接">
<p className="j13-module-title mb-3">
<Link2 size={15} style={{ color: "var(--accent)" }} /> 友情链接
<Link
href="/compose"
className="meta ml-auto text-[12px] font-normal transition-colors hover:text-[var(--accent)]"
>
申请
</Link>
</p>
<div className="flex flex-wrap gap-2">
{FRIEND_LINKS.map((l) =>
l.href.startsWith("http") ? (
<a
key={l.name}
href={l.href}
target="_blank"
rel="noopener noreferrer"
className="j13-chip"
>
{l.name}
</a>
) : (
<span key={l.name} className="j13-chip" data-tip="友链待配置">
{l.name}
</span>
)
)}
</div>
</section>
);
}
/* ---------- 桌面 / 移动两种装配 ---------- */
type PanelsProps = {
stats: OverviewStats;
hot: PostListItem[];
activeUsers: ActiveUser[];
announcements: AnnouncementSummary[];
newUsers: NewUser[];
checkin?: CheckinStatus | null;
};
export function CommunityPanels({
stats,
hot,
activeUsers,
announcements,
newUsers,
checkin,
}: PanelsProps) {
return (
<aside className="hidden w-[280px] shrink-0 lg:block xl:w-[300px]" aria-label="社区面板">
<div className="sticky top-24 space-y-4">
<PulseCard stats={stats} checkin={checkin} />
<Announcements items={announcements} />
<HotRanking posts={hot} />
<NewUsers users={newUsers} />
<ActiveUsers users={activeUsers} />
<FriendLinks />
</div>
</aside>
);
}
export function CommunityPanelsMobile({
stats,
hot,
activeUsers,
announcements,
newUsers,
checkin,
}: PanelsProps) {
return (
<div className="space-y-4 lg:hidden" aria-label="社区面板">
<PulseCard stats={stats} checkin={checkin} />
<Announcements items={announcements} />
<HotRanking posts={hot} />
<NewUsers users={newUsers} />
<ActiveUsers users={activeUsers} />
<FriendLinks />
</div>
);
}