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>
129 lines
4.2 KiB
TypeScript
129 lines
4.2 KiB
TypeScript
"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>
|
||
);
|
||
}
|