Files
dark-lord/app/forum/page.tsx
drjones 78a071ba02 Harden onion boot flow and deepen site surfaces
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
2026-04-07 21:35:52 -07:00

144 lines
5.8 KiB
TypeScript

"use client";
import { useEffect, useMemo, useState } from "react";
import Link from "next/link";
import ForumFeedToolbar from "@/components/forum/ForumFeedToolbar";
import { ForumPostCard } from "@/components/forum/ForumPostCard";
import { FORUM_RINGS } from "@/lib/forumRings";
import { threadMatchesQuery } from "@/lib/forumFeedFilter";
import {
isPinnedThread,
loadForum,
sortPinnedThreads,
type ForumThread,
} from "@/lib/forumState";
import { sortThreads, type ForumSortMode } from "@/lib/forumSort";
export default function VoidAggregateHomePage() {
const [threads, setThreads] = useState<ForumThread[]>([]);
const [sort, setSort] = useState<ForumSortMode>("hot");
const [ringFilter, setRingFilter] = useState<string | null>(null);
const [search, setSearch] = useState("");
const [hydrated, setHydrated] = useState(false);
const [, setVoteBump] = useState(0);
useEffect(() => {
setThreads(loadForum());
setHydrated(true);
}, []);
const filtered = useMemo(() => {
return threads.filter((t) => {
if (ringFilter !== null && t.topicSlug !== ringFilter) return false;
return threadMatchesQuery(t, search);
});
}, [threads, ringFilter, 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;
return (
<div className="flex flex-col gap-8 lg:flex-row">
<main className="min-w-0 flex-1 space-y-5">
<ForumFeedToolbar
sort={sort}
onSortChange={setSort}
search={search}
onSearchChange={setSearch}
ringFilter={ringFilter}
onRingChange={setRingFilter}
threadCount={totalShown}
hydrated={hydrated}
/>
{!hydrated ? (
<p className="font-mono text-sm text-[#8a7d6b]">Loading feed</p>
) : totalShown === 0 ? (
<div className="rounded-lg border border-dashed border-[#3d3228] p-10 text-center font-mono text-sm text-[#8a7d6b]">
<p>No threads match.</p>
<p className="mt-2 text-xs text-[#5c5044]">Clear search or pick another ring.</p>
<Link href="/forum/submit" className="mt-4 inline-block text-[#ff9a3c] underline">
Start a thread
</Link>
</div>
) : (
<>
{pinnedList.length ? (
<section className="space-y-2">
<h2 className="font-mono text-[10px] font-bold uppercase tracking-[0.28em] text-[#c45c26]/90">
Pinned · rules & mirrors
</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]">
Feed
</h2>
) : null}
{feedList.map((t) => (
<ForumPostCard key={t.id} thread={t} onVoteChange={() => setVoteBump((x) => x + 1)} />
))}
</section>
) : null}
</>
)}
</main>
<aside className="w-full shrink-0 space-y-4 lg:w-72">
<div className="rounded-lg border border-[#3d3228] bg-[#120e0a]/90 p-4">
<h3 className="font-mono text-xs font-bold uppercase tracking-widest text-[#ff9a3c]/80">Void Aggregate</h3>
<p className="mt-3 text-sm leading-relaxed text-[#b8a995]">
Ringed boards each namespace is an <code className="text-[#ff9a3c]">r/name</code> slice. Votes and new threads stay in your
browser profile; seeded &quot;Staff&quot; threads stay read-only so the front page stays legible.
</p>
<Link
href="/forum/submit"
className="mt-4 block w-full rounded border border-[#ff9a3c] bg-[#ff9a3c]/15 py-2 text-center font-mono text-xs font-bold uppercase text-[#ffcb8a] hover:bg-[#ff9a3c]/25"
>
Create post
</Link>
<Link
href="/chatter"
className="mt-3 block w-full rounded border border-[#3d3228] py-2 text-center font-mono text-xs text-[#b8a995] hover:border-[#ff9a3c]/40"
>
Hub chatter archive
</Link>
<Link
href="/account/hidden-services"
className="mt-2 block w-full rounded border border-[#3d3228] py-2 text-center font-mono text-xs text-[#8a7d6b] hover:border-[#ff9a3c]/40"
>
Identity on mirrors
</Link>
</div>
<div className="rounded-lg border border-[#3d3228] bg-[#120e0a]/90 p-4">
<h3 className="font-mono text-xs font-bold uppercase tracking-widest text-[#ff9a3c]/80">Rings</h3>
<ol className="mt-3 max-h-[min(50vh,22rem)] space-y-2 overflow-y-auto font-mono text-sm text-[#b8a995] pr-1">
{FORUM_RINGS.map((r) => (
<li key={r.slug} className="flex justify-between gap-2 border-b border-[#2a2218]/80 pb-2 last:border-0">
<Link href={`/forum/ring/${r.slug}`} className="min-w-0 truncate text-[#ffcb8a] hover:underline">
r/{r.shortName}
</Link>
<span className="shrink-0 text-[#8a7d6b]">{r.members > 999 ? `${Math.round(r.members / 1000)}k` : r.members}</span>
</li>
))}
</ol>
</div>
</aside>
</div>
);
}