This commit is contained in:
eric
2026-06-06 04:29:46 -05:00
parent 88aa96a2a1
commit 41493c35ca
50 changed files with 4666 additions and 416 deletions

View File

@@ -1,8 +1,9 @@
"use client";
import { useState } from "react";
import { useTranslation, Link } from "@/i18n/navigation";
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "@/i18n/navigation";
import Footer from "@/app/components/Footer";
import { apiFetch } from "@/app/lib/api-client";
interface Topic {
id: string;
@@ -15,9 +16,22 @@ interface Topic {
category: string;
lastActive: string;
pinned: boolean;
excerpt?: string;
}
const MOCK_TOPICS: Topic[] = [
interface DiscussionRecord {
id: string;
title: string;
author?: string;
city?: string;
tags?: string[];
excerpt?: string;
replies?: number;
views?: number;
created?: string;
}
const FALLBACK_TOPICS: Topic[] = [
{
id: "1",
title: "大理最佳共享办公空间推荐 Top 10",
@@ -127,16 +141,71 @@ const CATEGORIES = [
{ key: "目的地", zh: "目的地", en: "Destinations" },
];
function mapDiscussion(topic: DiscussionRecord, index: number): Topic {
const author = topic.author || "NomadCNA";
const tags = Array.isArray(topic.tags) ? topic.tags : [];
return {
id: topic.id,
title: topic.title,
author,
avatar: `https://api.dicebear.com/7.x/initials/svg?seed=${encodeURIComponent(author)}`,
views: Number(topic.views || 0),
replies: Number(topic.replies || 0),
likes: Math.max(3, Math.round(Number(topic.views || 0) / 30) + Number(topic.replies || 0)),
category: tags[0] || topic.city || "资源分享",
lastActive: topic.created ? new Date(topic.created).toLocaleDateString("zh-CN") : "刚刚",
pinned: index < 2,
excerpt: topic.excerpt,
};
}
export default function DiscussPage() {
const { t } = useTranslation("discuss");
const [activeCategory, setActiveCategory] = useState("all");
const [searchQuery, setSearchQuery] = useState("");
const [topics, setTopics] = useState<Topic[]>(FALLBACK_TOPICS);
const [showComposer, setShowComposer] = useState(false);
const [posting, setPosting] = useState(false);
const [draft, setDraft] = useState({ title: "", city: "", excerpt: "" });
const filteredTopics = MOCK_TOPICS.filter((topic) => {
useEffect(() => {
apiFetch<{ items: DiscussionRecord[] }>("/api/discussions")
.then((data) => {
if (data.items?.length) {
setTopics(data.items.map(mapDiscussion));
}
})
.catch(() => setTopics(FALLBACK_TOPICS));
}, []);
const filteredTopics = useMemo(() => topics.filter((topic) => {
const matchesCategory = activeCategory === "all" || topic.category === activeCategory;
const matchesSearch = topic.title.toLowerCase().includes(searchQuery.toLowerCase());
return matchesCategory && matchesSearch;
});
}), [activeCategory, searchQuery, topics]);
const createTopic = async (e: React.FormEvent) => {
e.preventDefault();
if (!draft.title.trim()) return;
setPosting(true);
try {
const data = await apiFetch<{ record: DiscussionRecord }>("/api/discussions", {
method: "POST",
body: JSON.stringify({
title: draft.title.trim(),
author: "本地用户",
city: draft.city.trim(),
tags: [draft.city.trim() || "资源分享"],
excerpt: draft.excerpt.trim(),
}),
});
setTopics((current) => [mapDiscussion(data.record, 0), ...current]);
setDraft({ title: "", city: "", excerpt: "" });
setShowComposer(false);
} finally {
setPosting(false);
}
};
return (
<div className="min-h-screen bg-[#fafafa] dark:bg-gray-950">
@@ -150,11 +219,49 @@ export default function DiscussPage() {
{t("subtitle")}
</p>
</div>
<button className="bg-[#ff4d4f] text-white px-4 sm:px-6 py-2 sm:py-2.5 rounded-full font-medium text-sm hover:bg-[#ff7a45] transition-colors">
<button
onClick={() => setShowComposer((value) => !value)}
className="bg-[#ff4d4f] text-white px-4 sm:px-6 py-2 sm:py-2.5 rounded-full font-medium text-sm hover:bg-[#ff7a45] transition-colors"
>
{t("newPost")}
</button>
</div>
{showComposer && (
<form onSubmit={createTopic} className="mb-6 rounded-2xl border border-gray-100 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900">
<div className="grid gap-3 sm:grid-cols-[1fr_180px]">
<input
required
value={draft.title}
onChange={(e) => setDraft((v) => ({ ...v, title: e.target.value }))}
placeholder="帖子标题"
className="rounded-xl border border-gray-200 bg-white px-4 py-3 text-sm text-gray-900 focus:border-[#ff4d4f] focus:outline-none dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
/>
<input
value={draft.city}
onChange={(e) => setDraft((v) => ({ ...v, city: e.target.value }))}
placeholder="关联城市"
className="rounded-xl border border-gray-200 bg-white px-4 py-3 text-sm text-gray-900 focus:border-[#ff4d4f] focus:outline-none dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
/>
</div>
<textarea
value={draft.excerpt}
onChange={(e) => setDraft((v) => ({ ...v, excerpt: e.target.value }))}
placeholder="写一点背景,后续会关联到城市弹窗的同城讨论"
rows={3}
className="mt-3 w-full resize-none rounded-xl border border-gray-200 bg-white px-4 py-3 text-sm text-gray-900 focus:border-[#ff4d4f] focus:outline-none dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
/>
<div className="mt-3 flex justify-end gap-2">
<button type="button" onClick={() => setShowComposer(false)} className="rounded-full border border-gray-200 px-4 py-2 text-sm text-gray-600 dark:border-gray-700 dark:text-gray-300">
</button>
<button disabled={posting} className="rounded-full bg-[#ff4d4f] px-4 py-2 text-sm font-semibold text-white disabled:opacity-60">
{posting ? "发布中..." : "发布"}
</button>
</div>
</form>
)}
<div className="mb-6">
<input
type="text"
@@ -205,6 +312,9 @@ export default function DiscussPage() {
<h3 className="font-semibold text-gray-900 dark:text-gray-100 text-sm sm:text-base mb-2 line-clamp-2">
{topic.title}
</h3>
{topic.excerpt && (
<p className="mb-2 line-clamp-2 text-xs text-gray-500 dark:text-gray-400">{topic.excerpt}</p>
)}
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-gray-500 dark:text-gray-400">
<span>{topic.author}</span>
<span className="hidden sm:inline">·</span>