s''
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user