nomadweb/frontend/src/components/CommunityHubClient.tsx
eric ff7b721acb feat: NomadCNA community layer, social matching, payments, and ebook
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>
2026-08-30 09:38:13 -05:00

129 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useMemo, useState } from "react";
import Link from "next/link";
import { useI18n } from "@/lib/i18n";
import type { Discussion, DiscussionDetail } from "@/lib/types";
const CATEGORIES = ["全部", "签证", "远程工作", "住宿", "安全", "社区"];
interface Props {
discussions: Discussion[];
featured?: DiscussionDetail | null;
}
export default function CommunityHubClient({ discussions, featured }: Props) {
const { t } = useI18n();
const [category, setCategory] = useState("全部");
const [q, setQ] = useState("");
const filtered = useMemo(() => {
let list = discussions;
if (category !== "全部") list = list.filter((d) => d.category === category);
if (q.trim()) {
const s = q.toLowerCase();
list = list.filter(
(d) =>
d.title.toLowerCase().includes(s) ||
d.excerpt.toLowerCase().includes(s) ||
d.tags.some((tag) => tag.toLowerCase().includes(s))
);
}
return list;
}, [discussions, category, q]);
return (
<div className="community-page">
<div className="container">
<nav className="detail-nav">
<Link href="/">{t.common.backHome}</Link>
<span> · </span>
<Link href="/meetups">{t.nav.meetups}</Link>
</nav>
<div className="section-header reveal">
<span className="section-tag">{t.community.tag}</span>
<h1>{t.community.title}</h1>
<p>{t.community.subtitle}</p>
</div>
<div className="community-banner reveal">
<div>
<strong>{t.community.bannerTitle}</strong>
<p>{t.community.bannerDesc}</p>
</div>
<Link href="/next-stop" className="btn btn-primary">{t.community.bannerCta}</Link>
</div>
{featured && featured.replies.length > 0 && (
<section className="community-featured reveal">
<span className="community-pinned">📌 {t.community.hot}</span>
<h3>{featured.title}</h3>
<p>{featured.excerpt}</p>
<div className="community-replies-preview">
{featured.replies.slice(0, 2).map((r) => (
<blockquote key={r.id}>
<span>{r.author_emoji} {r.author}</span>
{r.content}
</blockquote>
))}
</div>
<Link href={`/community/${featured.id}`}>{t.community.readMore} →</Link>
</section>
)}
<div className="community-toolbar reveal">
<Link href="/community/new" className="btn btn-primary btn-sm">{t.community.newTopic}</Link>
<input
className="tools-hub-search"
placeholder={t.community.search}
value={q}
onChange={(e) => setQ(e.target.value)}
/>
<div className="tools-hub-filters">
{CATEGORIES.map((c) => (
<button
key={c}
type="button"
className={`filter-btn${category === c ? " active" : ""}`}
onClick={() => setCategory(c)}
>
{c}
</button>
))}
</div>
</div>
<div className="community-list">
{filtered.map((d) => (
<Link key={d.id} href={`/community/${d.id}`} className="community-card reveal">
{d.is_pinned && <span className="community-pinned">📌</span>}
<div className="community-card-head">
<span>{d.author_emoji}</span>
<div>
<h3>{d.title}</h3>
<p className="community-meta">
{d.author} · {d.category} · {d.created_at}
</p>
</div>
</div>
<p className="community-excerpt">{d.excerpt}</p>
<footer>
<span>💬 {d.reply_count}</span>
<span>❤️ {d.like_count}</span>
{d.tags.map((tag) => (
<span key={tag} className="community-tag">{tag}</span>
))}
</footer>
</Link>
))}
</div>
{filtered.length === 0 && (
<p className="dest-empty">{t.community.empty}</p>
)}
</div>
</div>
);
}