Files
2026-03-15 04:25:45 -05:00

158 lines
5.2 KiB
TypeScript
Raw Permalink 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 { 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>
);
}