This commit is contained in:
eric
2026-03-15 04:25:45 -05:00
parent df12c7af4a
commit bbaa38b3b3
32 changed files with 4312 additions and 152 deletions

View File

@@ -0,0 +1,283 @@
"use client";
import Link from "next/link";
import { useParams, useRouter } from "next/navigation";
import { useCallback, useEffect, useState } from "react";
import {
CommunityMemoCard,
type CommunityComment,
type CommunityMemo,
} from "../../components/CommunityMemoCard";
import { CommunityCommentThread } from "../../components/CommunityCommentThread";
import { CommunityPageHero } from "../../components/CommunityPageHero";
import { emitCommunityChanged } from "../../components/communityEvents";
import styles from "../../components/memos.module.css";
function getStorage(key: string): string | null {
if (typeof window === "undefined") {
return null;
}
return localStorage.getItem(key);
}
export default function CommunityPostPage() {
const params = useParams();
const router = useRouter();
const memoId = Array.isArray(params.id) ? params.id[0] : params.id;
const [userId, setUserId] = useState<string | null>(null);
const [isVip, setIsVip] = useState(false);
const [showLoginPrompt, setShowLoginPrompt] = useState(false);
const [memo, setMemo] = useState<CommunityMemo | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [mutating, setMutating] = useState(false);
useEffect(() => {
const uid = getStorage("userId") || getStorage("memosAccount");
const type = getStorage("paidType");
setUserId(uid || null);
setIsVip(type === "vip");
}, []);
const loadMemo = useCallback(async () => {
if (!memoId || !userId || !isVip) {
return;
}
setLoading(true);
setError(null);
try {
const response = await fetch(
`/api/community/memos/${memoId}?user_id=${encodeURIComponent(userId)}`,
{ cache: "no-store" }
);
const data = await response.json().catch(() => null);
if (response.status === 403) {
setShowLoginPrompt(true);
setError("仅会员可访问完整讨论。");
return;
}
if (!response.ok || !data?.memo) {
setError(data?.error || "帖子不存在或已删除。");
return;
}
setMemo(data.memo as CommunityMemo);
} catch {
setError("网络异常,请稍后重试。");
} finally {
setLoading(false);
}
}, [isVip, memoId, userId]);
useEffect(() => {
void loadMemo();
}, [loadMemo]);
const handlePinToggle = useCallback(
async (_memoId: string, pinned: boolean) => {
if (!userId || !memo) {
return;
}
setMutating(true);
try {
const response = await fetch(`/api/community/memos/${memo.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ user_id: userId, pinned }),
});
const data = await response.json().catch(() => null);
if (!response.ok || !data?.memo) {
return;
}
setMemo(data.memo as CommunityMemo);
emitCommunityChanged();
} finally {
setMutating(false);
}
},
[memo, userId]
);
const handleReactionToggle = useCallback(
async (_memoId: string) => {
if (!userId || !memo) {
return;
}
setMutating(true);
try {
const response = await fetch(`/api/community/memos/${memo.id}/reactions`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ user_id: userId }),
});
const data = await response.json().catch(() => null);
if (!response.ok || !data?.reaction) {
return;
}
setMemo((current) =>
current
? {
...current,
liked_by_me: Boolean(data.reaction.liked),
reaction_count: Number(data.reaction.reaction_count ?? current.reaction_count ?? 0),
}
: current
);
emitCommunityChanged();
} finally {
setMutating(false);
}
},
[memo, userId]
);
const handleArchiveToggle = useCallback(
async (_memoId: string, nextState: "NORMAL" | "ARCHIVED") => {
if (!userId || !memo) {
return;
}
setMutating(true);
try {
const response = await fetch(`/api/community/memos/${memo.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ user_id: userId, state: nextState }),
});
const data = await response.json().catch(() => null);
if (!response.ok || !data?.memo) {
return;
}
setMemo(data.memo as CommunityMemo);
emitCommunityChanged();
} finally {
setMutating(false);
}
},
[memo, userId]
);
const handleDelete = useCallback(async () => {
if (!userId || !memo || !window.confirm("确定删除这条动态吗?")) {
return;
}
setMutating(true);
try {
const response = await fetch(
`/api/community/memos/${memo.id}?user_id=${encodeURIComponent(userId)}`,
{ method: "DELETE" }
);
if (!response.ok) {
return;
}
emitCommunityChanged();
router.replace("/community");
} finally {
setMutating(false);
}
}, [memo, router, userId]);
const handleCommentsChanged = useCallback((comments: CommunityComment[]) => {
setMemo((current) =>
current
? {
...current,
comments,
comment_count: comments.length,
}
: current
);
}, []);
return (
<div className={styles.pageStack}>
<CommunityPageHero
eyebrow="Post Detail"
title="完整讨论与评论"
description="这里保留这条动态的完整上下文,你可以继续点赞、评论、置顶或归档。"
chips={["完整上下文", "评论沉淀", "支持继续互动"]}
actions={[{ href: "/community", label: "返回动态流", secondary: true }]}
/>
{showLoginPrompt ? (
<section className={styles.vipBanner}>
<p>完整帖子和评论区仅对会员开放。</p>
<Link href="/" className={styles.ctaLink}>
立即加入会员
</Link>
</section>
) : null}
{!isVip || !userId ? (
<section className={styles.lockedPanel}>
<div className={styles.lockedPanelCopy}>
<span className={styles.lockedPanelEyebrow}>Members Only</span>
<h2 className={styles.lockedPanelTitle}>详情页需要会员权限</h2>
<p className={styles.lockedPanelDesc}>开通后即可查看完整讨论、参与评论并沉淀互动记录。</p>
</div>
<div className={styles.lockedPanelActions}>
<Link href="/" className={styles.heroPrimaryAction}>
去开通会员
</Link>
</div>
</section>
) : null}
{isVip && userId ? (
loading ? (
<section className={styles.filterPanel}>
<p className={styles.filterHint}>正在加载帖子详情...</p>
</section>
) : error || !memo ? (
<section className={styles.lockedPanel}>
<div className={styles.lockedPanelCopy}>
<span className={styles.lockedPanelEyebrow}>Unavailable</span>
<h2 className={styles.lockedPanelTitle}>帖子暂时不可用</h2>
<p className={styles.lockedPanelDesc}>{error || "帖子不存在或已被删除。"}</p>
</div>
<div className={styles.lockedPanelActions}>
<Link href="/community" className={styles.heroSecondaryAction}>
返回社群首页
</Link>
</div>
</section>
) : (
<>
<CommunityMemoCard
memo={memo}
isOwner={memo.user_id === userId}
actionDisabled={mutating}
hideDetailLink
onDelete={() => void handleDelete()}
onPinToggle={handlePinToggle}
onReactionToggle={handleReactionToggle}
onArchiveToggle={handleArchiveToggle}
/>
<CommunityCommentThread
memoId={memo.id}
userId={userId}
memoOwnerId={memo.user_id}
comments={memo.comments ?? []}
onCommentsChanged={handleCommentsChanged}
/>
</>
)
) : null}
</div>
);
}