258 lines
8.8 KiB
TypeScript
258 lines
8.8 KiB
TypeScript
"use client";
|
||
|
||
import Link from "next/link";
|
||
import { useEffect, useMemo, useState } from "react";
|
||
import { COMMUNITY_CHANGED_EVENT } from "./communityEvents";
|
||
import { maskCommunityUserId } from "./CommunityMemoCard";
|
||
import styles from "./memos.module.css";
|
||
|
||
type StatsPayload = {
|
||
stats: {
|
||
members: number | null;
|
||
total_memos: number;
|
||
normal_memos: number;
|
||
archived_memos: number;
|
||
pinned_memos: number;
|
||
contributors: number;
|
||
attachments: number;
|
||
comments: number;
|
||
reactions: number;
|
||
today_memos: number;
|
||
};
|
||
top_tags: Array<{ tag: string; count: number }>;
|
||
top_authors: Array<{ user_id: string; memo_count: number; latest_created: string }>;
|
||
latest_memos: Array<{ id: string; user_id: string; content: string }>;
|
||
};
|
||
|
||
type AttachmentItem = {
|
||
id: string;
|
||
url: string;
|
||
original_name: string;
|
||
created: string;
|
||
};
|
||
|
||
type CommunitySidebarProps = {
|
||
userId: string | null;
|
||
isVip: boolean;
|
||
};
|
||
|
||
function summarizeContent(content: string) {
|
||
const plain = content.replace(/[#>*`!\[\]()]/g, " ").replace(/\s+/g, " ").trim();
|
||
return plain.length > 42 ? `${plain.slice(0, 42)}...` : plain;
|
||
}
|
||
|
||
export function CommunitySidebar({ userId, isVip }: CommunitySidebarProps) {
|
||
const [stats, setStats] = useState<StatsPayload | null>(null);
|
||
const [attachments, setAttachments] = useState<AttachmentItem[]>([]);
|
||
const [loading, setLoading] = useState(false);
|
||
|
||
useEffect(() => {
|
||
if (!isVip || !userId) {
|
||
return;
|
||
}
|
||
|
||
let cancelled = false;
|
||
|
||
const load = async () => {
|
||
setLoading(true);
|
||
try {
|
||
const [statsRes, attachmentsRes] = await Promise.all([
|
||
fetch(`/api/community/stats?user_id=${encodeURIComponent(userId)}`, { cache: "no-store" }),
|
||
fetch(`/api/community/attachments?user_id=${encodeURIComponent(userId)}&per_page=6`, {
|
||
cache: "no-store",
|
||
}),
|
||
]);
|
||
|
||
const [statsData, attachmentsData] = await Promise.all([
|
||
statsRes.json().catch(() => null),
|
||
attachmentsRes.json().catch(() => null),
|
||
]);
|
||
|
||
if (cancelled) {
|
||
return;
|
||
}
|
||
|
||
if (statsRes.ok && statsData?.ok) {
|
||
setStats(statsData as StatsPayload);
|
||
}
|
||
if (attachmentsRes.ok && attachmentsData?.ok) {
|
||
setAttachments(attachmentsData.items ?? []);
|
||
}
|
||
} finally {
|
||
if (!cancelled) {
|
||
setLoading(false);
|
||
}
|
||
}
|
||
};
|
||
|
||
void load();
|
||
|
||
const handler = () => {
|
||
void load();
|
||
};
|
||
window.addEventListener(COMMUNITY_CHANGED_EVENT, handler);
|
||
return () => {
|
||
cancelled = true;
|
||
window.removeEventListener(COMMUNITY_CHANGED_EVENT, handler);
|
||
};
|
||
}, [isVip, userId]);
|
||
|
||
const statCards = useMemo(() => {
|
||
if (!stats) {
|
||
return [];
|
||
}
|
||
|
||
return [
|
||
{
|
||
label: "动态",
|
||
value: stats.stats.normal_memos,
|
||
hint: `今日 +${stats.stats.today_memos}`,
|
||
},
|
||
{
|
||
label: "成员",
|
||
value: stats.stats.members ?? stats.stats.contributors,
|
||
hint: `${stats.stats.contributors} 位发言者`,
|
||
},
|
||
{
|
||
label: "评论",
|
||
value: stats.stats.comments,
|
||
hint: `${stats.stats.reactions} 次点赞`,
|
||
},
|
||
{
|
||
label: "附件",
|
||
value: stats.stats.attachments,
|
||
hint: `${stats.stats.pinned_memos} 条置顶`,
|
||
},
|
||
];
|
||
}, [stats]);
|
||
|
||
if (!isVip || !userId) {
|
||
return (
|
||
<div className={styles.sidebarStack}>
|
||
<section className={styles.sidebarCard}>
|
||
<span className={styles.sidebarEyebrow}>Private Planet</span>
|
||
<h3 className={styles.sidebarTitle}>加入异度星球</h3>
|
||
<p className={styles.sidebarDesc}>
|
||
解锁私密动态、精选复盘、图片附件和长期沉淀的问答内容。
|
||
</p>
|
||
<div className={styles.sidebarFeatureList}>
|
||
<span className={styles.sidebarFeature}>每周精选更新</span>
|
||
<span className={styles.sidebarFeature}>成员动态时间流</span>
|
||
<span className={styles.sidebarFeature}>附件资料库</span>
|
||
</div>
|
||
<Link href="/" className={styles.sidebarCTA}>
|
||
立即加入会员
|
||
</Link>
|
||
</section>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className={styles.sidebarStack}>
|
||
<section className={styles.sidebarCard}>
|
||
<span className={styles.sidebarEyebrow}>Planet Overview</span>
|
||
<h3 className={styles.sidebarTitle}>星球概览</h3>
|
||
<p className={styles.sidebarDesc}>围绕出海、自动化、设备和 AI 的持续交流空间。</p>
|
||
<div className={styles.sidebarStatsGrid}>
|
||
{statCards.map((card) => (
|
||
<div key={card.label} className={styles.sidebarStatCard}>
|
||
<span className={styles.sidebarStatLabel}>{card.label}</span>
|
||
<strong className={styles.sidebarStatValue}>{card.value}</strong>
|
||
<span className={styles.sidebarStatHint}>{card.hint}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
{loading && !stats ? <p className={styles.sidebarLoading}>正在同步星球数据...</p> : null}
|
||
</section>
|
||
|
||
<section className={styles.sidebarCard}>
|
||
<div className={styles.sidebarSectionHeader}>
|
||
<h3 className={styles.sidebarTitle}>热门标签</h3>
|
||
<Link href="/community/explore" className={styles.sidebarLink}>
|
||
去发现
|
||
</Link>
|
||
</div>
|
||
<div className={styles.sidebarTagList}>
|
||
{(stats?.top_tags ?? []).slice(0, 8).map((item) => (
|
||
<Link
|
||
key={item.tag}
|
||
href={`/community/explore?tag=${encodeURIComponent(item.tag)}`}
|
||
className={styles.sidebarTag}
|
||
>
|
||
#{item.tag}
|
||
<span>{item.count}</span>
|
||
</Link>
|
||
))}
|
||
{!stats?.top_tags?.length ? (
|
||
<p className={styles.sidebarEmpty}>发布带标签的内容后,这里会自动聚合热门话题。</p>
|
||
) : null}
|
||
</div>
|
||
</section>
|
||
|
||
<section className={styles.sidebarCard}>
|
||
<div className={styles.sidebarSectionHeader}>
|
||
<h3 className={styles.sidebarTitle}>活跃成员</h3>
|
||
<Link href="/community" className={styles.sidebarLink}>
|
||
看动态
|
||
</Link>
|
||
</div>
|
||
<div className={styles.sidebarList}>
|
||
{(stats?.top_authors ?? []).slice(0, 5).map((item) => (
|
||
<div key={item.user_id} className={styles.sidebarListItem}>
|
||
<div className={styles.sidebarAvatar}>{maskCommunityUserId(item.user_id).slice(-2)}</div>
|
||
<div>
|
||
<div className={styles.sidebarListTitle}>{maskCommunityUserId(item.user_id)}</div>
|
||
<div className={styles.sidebarListMeta}>{item.memo_count} 条动态</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
{!stats?.top_authors?.length ? (
|
||
<p className={styles.sidebarEmpty}>第一条动态发布后,这里会出现活跃成员。</p>
|
||
) : null}
|
||
</div>
|
||
</section>
|
||
|
||
<section className={styles.sidebarCard}>
|
||
<div className={styles.sidebarSectionHeader}>
|
||
<h3 className={styles.sidebarTitle}>最新帖子</h3>
|
||
<Link href="/community" className={styles.sidebarLink}>
|
||
全部动态
|
||
</Link>
|
||
</div>
|
||
<div className={styles.sidebarList}>
|
||
{(stats?.latest_memos ?? []).map((item) => (
|
||
<Link key={item.id} href={`/community/post/${item.id}`} className={styles.explorerLink}>
|
||
<div className={styles.sidebarAvatar}>{maskCommunityUserId(item.user_id).slice(-2)}</div>
|
||
<div>
|
||
<div className={styles.sidebarListTitle}>{maskCommunityUserId(item.user_id)}</div>
|
||
<div className={styles.sidebarListMeta}>{summarizeContent(item.content)}</div>
|
||
</div>
|
||
</Link>
|
||
))}
|
||
{!stats?.latest_memos?.length ? (
|
||
<p className={styles.sidebarEmpty}>发布动态后,这里会展示最近更新的帖子。</p>
|
||
) : null}
|
||
</div>
|
||
</section>
|
||
|
||
<section className={styles.sidebarCard}>
|
||
<div className={styles.sidebarSectionHeader}>
|
||
<h3 className={styles.sidebarTitle}>最新附件</h3>
|
||
<Link href="/community/attachments" className={styles.sidebarLink}>
|
||
全部附件
|
||
</Link>
|
||
</div>
|
||
<div className={styles.sidebarAttachmentGrid}>
|
||
{attachments.map((item) => (
|
||
<a key={item.id} href={item.url} target="_blank" rel="noreferrer" className={styles.sidebarAttachment}>
|
||
<img src={item.url} alt={item.original_name} className={styles.sidebarAttachmentImage} />
|
||
</a>
|
||
))}
|
||
</div>
|
||
{!attachments.length ? <p className={styles.sidebarEmpty}>上传图片后,这里会自动汇总最近附件。</p> : null}
|
||
</section>
|
||
</div>
|
||
);
|
||
}
|