From d96666c32962daf01c23b5bac77637870e190b01 Mon Sep 17 00:00:00 2001 From: eric Date: Sun, 30 Aug 2026 20:31:29 -0500 Subject: [PATCH] Harden write flows and unlock digital VIP lessons client-side. Co-authored-by: Cursor --- backend/app/services/community_store.py | 2 +- .../src/app/digital/course/[m]/[l]/page.tsx | 10 +- frontend/src/components/ChatThreadClient.tsx | 52 +++++++- .../src/components/CommunityNewClient.tsx | 33 ++++- .../src/components/DigitalCourseClient.tsx | 71 ++++++++--- frontend/src/components/DigitalJobsClient.tsx | 48 ++++--- .../src/components/DigitalLessonClient.tsx | 118 ++++++++++++++++-- .../src/components/DiscussionDetailClient.tsx | 68 ++++++++-- frontend/src/components/GigPostClient.tsx | 23 +++- frontend/src/components/MeetupsHostClient.tsx | 28 ++++- .../components/NotificationSettingsClient.tsx | 64 +++++++--- frontend/src/components/ServicesClient.tsx | 90 ++++++++++--- 12 files changed, 487 insertions(+), 120 deletions(-) diff --git a/backend/app/services/community_store.py b/backend/app/services/community_store.py index ea13416..e18204f 100644 --- a/backend/app/services/community_store.py +++ b/backend/app/services/community_store.py @@ -263,7 +263,7 @@ def create_discussion(user_id: str, user_name: str, payload: dict) -> dict: { "legacyId": did, "title": payload["title"], - "excerpt": payload.get("excerpt") or payload.get("content", "")[:200], + "excerpt": (payload.get("excerpt") or payload.get("content") or "")[:1000], "author": user_name, "authorUserId": user_id, "authorEmoji": payload.get("author_emoji", "🧑‍💻"), diff --git a/frontend/src/app/digital/course/[m]/[l]/page.tsx b/frontend/src/app/digital/course/[m]/[l]/page.tsx index 0f84aa6..cba9724 100644 --- a/frontend/src/app/digital/course/[m]/[l]/page.tsx +++ b/frontend/src/app/digital/course/[m]/[l]/page.tsx @@ -1,7 +1,6 @@ import type { Metadata } from "next"; import SiteShell from "@/components/SiteShell"; import DigitalLessonClient from "@/components/DigitalLessonClient"; -import { api } from "@/lib/api"; export const metadata: Metadata = { title: "课程 · nomadro 学院", @@ -15,16 +14,9 @@ export default async function DigitalLessonPage({ const { m, l } = await params; const mi = Number(m); const li = Number(l); - let lesson = null; - let locked = false; - try { - lesson = await api.getDigitalLesson(mi, li); - } catch { - locked = true; - } return ( - + ); } diff --git a/frontend/src/components/ChatThreadClient.tsx b/frontend/src/components/ChatThreadClient.tsx index 2aa1895..6262ae7 100644 --- a/frontend/src/components/ChatThreadClient.tsx +++ b/frontend/src/components/ChatThreadClient.tsx @@ -5,24 +5,37 @@ import Link from "next/link"; import { api } from "@/lib/api"; import { useAuth } from "@/lib/auth"; import { useI18n } from "@/lib/i18n"; +import { useToast } from "@/lib/toast"; import type { ChatMessage } from "@/lib/types"; export default function ChatThreadClient({ convId }: { convId: string }) { - const { token } = useAuth(); + const { token, user } = useAuth(); const { t } = useI18n(); + const { toast } = useToast(); const [messages, setMessages] = useState([]); const [text, setText] = useState(""); + const [sending, setSending] = useState(false); + const [loading, setLoading] = useState(Boolean(token)); const bottomRef = useRef(null); const load = () => { if (!token) return; - api.getMessages(token, convId).then(setMessages).catch(() => setMessages([])); + api + .getMessages(token, convId) + .then(setMessages) + .catch(() => setMessages([])) + .finally(() => setLoading(false)); }; useEffect(() => { + if (!token) { + setLoading(false); + return; + } load(); const id = setInterval(load, 4000); return () => clearInterval(id); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [token, convId]); useEffect(() => { @@ -30,14 +43,34 @@ export default function ChatThreadClient({ convId }: { convId: string }) { }, [messages]); const send = async () => { - if (!token || !text.trim()) return; + if (!token || !text.trim() || sending) return; + setSending(true); try { await api.sendMessage(token, convId, text.trim()); setText(""); load(); - } catch { /* ignore */ } + } catch { + toast("发送失败,请稍后重试", "error"); + } finally { + setSending(false); + } }; + if (!user || !token) { + return ( +
+
+
+

{t.chat.loginTitle}

+ + {t.nav.login} + +
+
+
+ ); + } + return (
@@ -45,6 +78,10 @@ export default function ChatThreadClient({ convId }: { convId: string }) { ← {t.chat.back}
+ {loading && messages.length === 0 &&

加载消息…

} + {!loading && messages.length === 0 && ( +

打个招呼开始对话吧

+ )} {messages.map((m) => (
{m.body} @@ -57,9 +94,12 @@ export default function ChatThreadClient({ convId }: { convId: string }) { value={text} onChange={(e) => setText(e.target.value)} placeholder={t.chat.placeholder} - onKeyDown={(e) => e.key === "Enter" && send()} + onKeyDown={(e) => e.key === "Enter" && void send()} + disabled={sending} /> - +
diff --git a/frontend/src/components/CommunityNewClient.tsx b/frontend/src/components/CommunityNewClient.tsx index 4ac906a..bc5b88a 100644 --- a/frontend/src/components/CommunityNewClient.tsx +++ b/frontend/src/components/CommunityNewClient.tsx @@ -16,18 +16,35 @@ export default function CommunityNewClient() { const [title, setTitle] = useState(""); const [content, setContent] = useState(""); const [category, setCategory] = useState("社区"); + const [busy, setBusy] = useState(false); const submit = async () => { if (!token) { router.push("/login?next=/community/new"); return; } + const tTitle = title.trim(); + const body = content.trim(); + if (!tTitle || body.length < 10) { + toast("标题必填,正文至少 10 字", "info"); + return; + } + setBusy(true); try { - const res = await api.createDiscussion(token, { title, content, excerpt: content, category }); + const excerpt = body.slice(0, 1000); + const res = await api.createDiscussion(token, { + title: tTitle, + content: body, + excerpt, + category, + }); toast(t.community.createOk, "success"); - router.push(`/community/${res.discussion.id}`); + const id = res.discussion?.id; + router.push(id ? `/community/${id}` : "/community"); } catch { toast(t.community.createFail, "error"); + } finally { + setBusy(false); } }; @@ -40,14 +57,18 @@ export default function CommunityNewClient() {
- setTitle(e.target.value)} /> + setTitle(e.target.value)} maxLength={120} /> -