Files
gitlab-instance-0a899031_no…/app/community/components/CommunitySidebar.tsx
2026-03-15 04:25:45 -05:00

258 lines
8.8 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.
"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>
);
}