's'
This commit is contained in:
283
app/community/post/[id]/page.tsx
Normal file
283
app/community/post/[id]/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user