Port meetups, discussions, gigs, dating/chat, VIP pay (ZPay/XorPay), MiroTalk live, digital academy, and ebook reader. Add persistent community_store, git-first deploy docs, and env template for production secrets. Co-authored-by: Cursor <cursoragent@cursor.com>
104 lines
3.5 KiB
TypeScript
104 lines
3.5 KiB
TypeScript
"use client";
|
||
|
||
import { useState } from "react";
|
||
import Link from "next/link";
|
||
import { useRouter } from "next/navigation";
|
||
import { api } from "@/lib/api";
|
||
import { useAuth } from "@/lib/auth";
|
||
import { useToast } from "@/lib/toast";
|
||
import { useI18n } from "@/lib/i18n";
|
||
|
||
export default function DiscussionDetailClient({ discussionId, initial }: { discussionId: string; initial: import("@/lib/types").DiscussionDetail }) {
|
||
const { user, token } = useAuth();
|
||
const { toast } = useToast();
|
||
const { t } = useI18n();
|
||
const router = useRouter();
|
||
const [discussion, setDiscussion] = useState(initial);
|
||
const [reply, setReply] = useState("");
|
||
const [likes, setLikes] = useState(discussion.like_count);
|
||
const [loading, setLoading] = useState(false);
|
||
|
||
const refresh = async () => {
|
||
const d = await api.getDiscussion(discussionId);
|
||
setDiscussion(d);
|
||
setLikes(d.like_count);
|
||
};
|
||
|
||
const submitReply = async () => {
|
||
if (!token || !reply.trim()) return;
|
||
setLoading(true);
|
||
try {
|
||
await api.postDiscussionReply(token, discussionId, reply.trim());
|
||
setReply("");
|
||
await refresh();
|
||
toast(t.community.replyOk, "success");
|
||
} catch {
|
||
toast(t.community.replyFail, "error");
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const toggleLike = async () => {
|
||
if (!token) {
|
||
router.push(`/login?next=/community/${discussionId}`);
|
||
return;
|
||
}
|
||
try {
|
||
const res = await api.likeDiscussion(token, discussionId);
|
||
setLikes(res.like_count);
|
||
} catch { /* ignore */ }
|
||
};
|
||
|
||
return (
|
||
<div className="community-detail-page">
|
||
<div className="container">
|
||
<nav className="detail-nav">
|
||
<Link href="/community">← {t.community.back}</Link>
|
||
</nav>
|
||
|
||
<article className="community-detail reveal">
|
||
<header>
|
||
<span className="community-detail-cat">{discussion.category}</span>
|
||
<h1>{discussion.title}</h1>
|
||
<p className="community-meta">
|
||
{discussion.author_emoji} {discussion.author} · {discussion.created_at}
|
||
</p>
|
||
</header>
|
||
<div className="community-detail-body">
|
||
<p>{discussion.excerpt}</p>
|
||
</div>
|
||
<div className="community-detail-stats">
|
||
<span>💬 {discussion.reply_count} {t.community.replies}</span>
|
||
<button type="button" className="btn btn-sm" onClick={toggleLike}>❤️ {likes}</button>
|
||
</div>
|
||
</article>
|
||
|
||
<section className="community-replies reveal">
|
||
<h2>{t.community.replies} ({discussion.replies.length})</h2>
|
||
{discussion.replies.map((r) => (
|
||
<div key={r.id} className="community-reply">
|
||
<div className="community-reply-head">
|
||
<span>{r.author_emoji} {r.author}</span>
|
||
<time>{r.created_at}</time>
|
||
</div>
|
||
<p>{r.content}</p>
|
||
</div>
|
||
))}
|
||
</section>
|
||
|
||
{user && token ? (
|
||
<div className="community-compose reveal">
|
||
<textarea value={reply} onChange={(e) => setReply(e.target.value)} rows={3} placeholder={t.community.replyPlaceholder} />
|
||
<button type="button" className="btn btn-primary" disabled={loading} onClick={submitReply}>{t.community.replySend}</button>
|
||
</div>
|
||
) : (
|
||
<div className="community-reply-hint reveal">
|
||
<Link href={`/login?next=/community/${discussionId}`} className="btn btn-primary">{t.nav.login}</Link>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|