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
144 lines
5.8 KiB
TypeScript
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 "Staff" 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>
|
|
);
|
|
}
|