158 lines
5.2 KiB
TypeScript
158 lines
5.2 KiB
TypeScript
"use client";
|
||
|
||
import Link from "next/link";
|
||
import { useCallback, useEffect, useState } from "react";
|
||
import { CommunityMemoForm } from "./components/CommunityMemoForm";
|
||
import { CommunityTimeline } from "./components/CommunityTimeline";
|
||
import { CommunityPageHero } from "./components/CommunityPageHero";
|
||
import { emitCommunityChanged } from "./components/communityEvents";
|
||
import type { CommunityMemo } from "./components/CommunityMemoCard";
|
||
import { COMMUNITY_CONFIG } from "@/config/community.config";
|
||
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 CommunityPage() {
|
||
const [userId, setUserId] = useState<string | null>(null);
|
||
const [isVip, setIsVip] = useState(false);
|
||
const [timelineKey, setTimelineKey] = useState(0);
|
||
const [showLoginPrompt, setShowLoginPrompt] = useState(false);
|
||
const [restoreContent, setRestoreContent] = useState<string | null>(null);
|
||
const [prependedMemos, setPrependedMemos] = useState<CommunityMemo[]>([]);
|
||
|
||
useEffect(() => {
|
||
const uid = getStorage("userId") || getStorage("memosAccount");
|
||
const type = getStorage("paidType");
|
||
setUserId(uid || null);
|
||
setIsVip(type === "vip");
|
||
}, []);
|
||
|
||
const handlePublish = useCallback(
|
||
async (content: string) => {
|
||
if (!userId) {
|
||
setShowLoginPrompt(true);
|
||
throw new Error("请先登录");
|
||
}
|
||
|
||
const tempMemo: CommunityMemo = {
|
||
id: `temp-${Date.now()}`,
|
||
user_id: userId,
|
||
content,
|
||
pinned: false,
|
||
state: "NORMAL",
|
||
created: new Date().toISOString(),
|
||
updated: new Date().toISOString(),
|
||
comment_count: 0,
|
||
reaction_count: 0,
|
||
liked_by_me: false,
|
||
};
|
||
setPrependedMemos([tempMemo]);
|
||
|
||
try {
|
||
const response = await fetch("/api/community/memos", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ user_id: userId, content }),
|
||
});
|
||
const data = await response.json().catch(() => null);
|
||
|
||
if (response.status === 403) {
|
||
setShowLoginPrompt(true);
|
||
setPrependedMemos([]);
|
||
setRestoreContent(content);
|
||
throw new Error(data?.error || "仅会员可发布内容");
|
||
}
|
||
|
||
if (!response.ok) {
|
||
setPrependedMemos([]);
|
||
setRestoreContent(content);
|
||
throw new Error(data?.error || "发布失败,请稍后重试");
|
||
}
|
||
|
||
setPrependedMemos([]);
|
||
setTimelineKey((current) => current + 1);
|
||
emitCommunityChanged();
|
||
} catch (error) {
|
||
setPrependedMemos([]);
|
||
setRestoreContent(content);
|
||
throw error;
|
||
}
|
||
},
|
||
[userId]
|
||
);
|
||
|
||
if (!COMMUNITY_CONFIG.enabled) {
|
||
return (
|
||
<div className={styles.main}>
|
||
<p className={styles.pageDesc}>社区功能暂未开启。</p>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className={styles.pageStack}>
|
||
<CommunityPageHero
|
||
eyebrow="Private Community"
|
||
title="像知识星球一样持续沉淀内容"
|
||
description="围绕出海、AI、自动化和设备实践,沉淀动态、评论、附件与可检索的话题索引。"
|
||
chips={["会员可见", "支持 Markdown", "图片自动沉淀", "可点赞评论"]}
|
||
actions={[
|
||
{ href: "/community/explore", label: "浏览话题", secondary: true },
|
||
{ href: "/community/attachments", label: "查看附件" },
|
||
]}
|
||
/>
|
||
|
||
{showLoginPrompt ? (
|
||
<section className={styles.vipBanner}>
|
||
<p>发布和完整浏览社群内容需要会员权限。</p>
|
||
<Link href="/" className={styles.ctaLink}>
|
||
立即加入会员
|
||
</Link>
|
||
</section>
|
||
) : null}
|
||
|
||
{isVip && userId ? (
|
||
<>
|
||
<CommunityMemoForm
|
||
onSubmit={handlePublish}
|
||
disabled={false}
|
||
maxLength={COMMUNITY_CONFIG.maxContentLength}
|
||
userId={userId}
|
||
restoreContent={restoreContent}
|
||
onRestoreConsumed={() => setRestoreContent(null)}
|
||
/>
|
||
<CommunityTimeline
|
||
userId={userId}
|
||
refreshKey={timelineKey}
|
||
prependedMemos={prependedMemos}
|
||
onVipRequired={() => setShowLoginPrompt(true)}
|
||
/>
|
||
</>
|
||
) : (
|
||
<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>
|
||
<Link href="/community/explore" className={styles.heroSecondaryAction}>
|
||
先看内容结构
|
||
</Link>
|
||
</div>
|
||
</section>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|