Add persistent onion key backup and restore, improve startup resilience, and flesh out the major site verticals with richer navigation, search coverage, and operator documentation. Made-with: Cursor
142 lines
5.2 KiB
TypeScript
142 lines
5.2 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import Link from "next/link";
|
|
import { useParams } from "next/navigation";
|
|
import ForumFeedToolbar from "@/components/forum/ForumFeedToolbar";
|
|
import { ForumPostCard } from "@/components/forum/ForumPostCard";
|
|
import { threadMatchesQuery } from "@/lib/forumFeedFilter";
|
|
import { getRing } from "@/lib/forumRings";
|
|
import {
|
|
isPinnedThread,
|
|
loadForum,
|
|
sortPinnedThreads,
|
|
type ForumThread,
|
|
} from "@/lib/forumState";
|
|
import { sortThreads, type ForumSortMode } from "@/lib/forumSort";
|
|
|
|
export default function VoidAggregateRingPage() {
|
|
const params = useParams();
|
|
const slug = typeof params.slug === "string" ? params.slug : "";
|
|
const ring = getRing(slug);
|
|
|
|
const [threads, setThreads] = useState<ForumThread[]>([]);
|
|
const [sort, setSort] = useState<ForumSortMode>("hot");
|
|
const [search, setSearch] = useState("");
|
|
const [hydrated, setHydrated] = useState(false);
|
|
const [, setVoteBump] = useState(0);
|
|
|
|
useEffect(() => {
|
|
setThreads(loadForum());
|
|
setHydrated(true);
|
|
}, []);
|
|
|
|
const filtered = useMemo(() => {
|
|
if (!ring) return [];
|
|
return threads
|
|
.filter((t) => t.topicSlug === ring.slug)
|
|
.filter((t) => threadMatchesQuery(t, search));
|
|
}, [threads, ring, search]);
|
|
|
|
const { pinnedList, feedList } = useMemo(() => {
|
|
const pinned = sortPinnedThreads(filtered.filter(isPinnedThread));
|
|
const rest = filtered.filter((t) => !isPinnedThread(t));
|
|
return { pinnedList: pinned, feedList: sortThreads(rest, sort) };
|
|
}, [filtered, sort]);
|
|
|
|
const totalShown = pinnedList.length + feedList.length;
|
|
|
|
if (!ring) {
|
|
return (
|
|
<div className="rounded-lg border border-[#c45c26]/40 bg-[#120e0a] p-8 text-center font-mono">
|
|
<p className="text-[#ffcb8a]">Unknown ring · /r/{slug}</p>
|
|
<Link href="/forum" className="mt-4 inline-block text-sm text-[#ff9a3c] underline">
|
|
← front page
|
|
</Link>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<header className="rounded-lg border border-[#3d3228] bg-[#120e0a]/90 p-6">
|
|
<p className="font-mono text-[10px] uppercase tracking-[0.35em] text-[#ff9a3c]/70">r/{ring.shortName}</p>
|
|
<h1 className="mt-2 font-mono text-2xl font-black text-[#ffcb8a] md:text-3xl">{ring.title}</h1>
|
|
<p className="mt-3 max-w-2xl text-sm leading-relaxed text-[#b8a995]">{ring.about}</p>
|
|
<div className="mt-4 flex flex-wrap gap-4 font-mono text-[11px] text-[#8a7d6b]">
|
|
<span>
|
|
<strong className="text-[#ffcb8a]/90">{ring.members.toLocaleString()}</strong> members
|
|
</span>
|
|
<span>
|
|
<strong className="text-[#39ff14]/80">{ring.activeNow.toLocaleString()}</strong> est. online
|
|
</span>
|
|
</div>
|
|
<div className="mt-5 flex flex-wrap gap-2">
|
|
<Link
|
|
href={`/forum/submit?ring=${encodeURIComponent(ring.slug)}`}
|
|
className="rounded border border-[#ff9a3c] bg-[#ff9a3c]/15 px-4 py-2 font-mono text-xs font-bold uppercase text-[#ffcb8a] hover:bg-[#ff9a3c]/25"
|
|
>
|
|
Post in r/{ring.shortName}
|
|
</Link>
|
|
<Link href="/forum" className="rounded border border-[#3d3228] px-4 py-2 font-mono text-xs text-[#b8a995] hover:border-[#ff9a3c]/40">
|
|
← All rings
|
|
</Link>
|
|
</div>
|
|
</header>
|
|
|
|
<ForumFeedToolbar
|
|
sort={sort}
|
|
onSortChange={setSort}
|
|
search={search}
|
|
onSearchChange={setSearch}
|
|
ringFilter={ring.slug}
|
|
threadCount={totalShown}
|
|
hydrated={hydrated}
|
|
extraRight={
|
|
<Link href="/forum" className="rounded px-2 py-1 text-[#8a7d6b] hover:text-[#ffcb8a]">
|
|
Global feed
|
|
</Link>
|
|
}
|
|
/>
|
|
|
|
{!hydrated ? (
|
|
<p className="font-mono text-sm text-[#8a7d6b]">Loading…</p>
|
|
) : totalShown === 0 ? (
|
|
<p className="rounded-lg border border-dashed border-[#3d3228] p-8 text-center font-mono text-sm text-[#8a7d6b]">
|
|
No threads match in r/{ring.shortName}.{" "}
|
|
<Link href={`/forum/submit?ring=${encodeURIComponent(ring.slug)}`} className="text-[#ff9a3c] underline">
|
|
Start one
|
|
</Link>
|
|
.
|
|
</p>
|
|
) : (
|
|
<div className="space-y-5">
|
|
{pinnedList.length ? (
|
|
<section className="space-y-2">
|
|
<h2 className="font-mono text-[10px] font-bold uppercase tracking-[0.28em] text-[#c45c26]/90">
|
|
Pinned here
|
|
</h2>
|
|
<div className="space-y-3">
|
|
{pinnedList.map((t) => (
|
|
<ForumPostCard key={t.id} thread={t} onVoteChange={() => setVoteBump((x) => x + 1)} />
|
|
))}
|
|
</div>
|
|
</section>
|
|
) : null}
|
|
|
|
{feedList.length ? (
|
|
<section className="space-y-3">
|
|
{pinnedList.length ? (
|
|
<h2 className="font-mono text-[10px] font-bold uppercase tracking-[0.28em] text-[#8a7d6b]">Threads</h2>
|
|
) : null}
|
|
{feedList.map((t) => (
|
|
<ForumPostCard key={t.id} thread={t} onVoteChange={() => setVoteBump((x) => x + 1)} />
|
|
))}
|
|
</section>
|
|
) : null}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|