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
143 lines
5.6 KiB
TypeScript
143 lines
5.6 KiB
TypeScript
"use client";
|
||
|
||
import Link from "next/link";
|
||
import { useMemo } from "react";
|
||
import { useSearchParams } from "next/navigation";
|
||
import { formatRingLabel } from "@/lib/forumRings";
|
||
import { SHOP_CHATTER_COUNT, getChatterSlice } from "@/lib/shopChatterThreads";
|
||
|
||
const PAGE_SIZE = 12;
|
||
|
||
function formatAgo(ts: number): string {
|
||
const s = Math.floor((Date.now() - ts) / 1000);
|
||
if (s < 60) return `${s}s ago`;
|
||
const m = Math.floor(s / 60);
|
||
if (m < 60) return `${m}m ago`;
|
||
const h = Math.floor(m / 60);
|
||
if (h < 48) return `${h}h ago`;
|
||
const d = Math.floor(h / 24);
|
||
return `${d}d ago`;
|
||
}
|
||
|
||
export default function HubChatterArchivePage() {
|
||
const sp = useSearchParams();
|
||
const page = Math.max(1, parseInt(sp.get("page") ?? "1", 10) || 1);
|
||
|
||
const { items, totalPages, total } = useMemo(
|
||
() => getChatterSlice(page, PAGE_SIZE),
|
||
[page],
|
||
);
|
||
|
||
const pages = useMemo(() => {
|
||
const window = 2;
|
||
const lo = Math.max(1, page - window);
|
||
const hi = Math.min(totalPages, page + window);
|
||
const out: number[] = [];
|
||
for (let i = lo; i <= hi; i++) out.push(i);
|
||
return out;
|
||
}, [page, totalPages]);
|
||
|
||
return (
|
||
<div className="min-h-screen bg-[#050403] text-[#d4c4b0]">
|
||
<div
|
||
className="pointer-events-none fixed inset-0 z-0 opacity-[0.08]"
|
||
style={{
|
||
backgroundImage:
|
||
"repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(255,180,120,0.12) 3px)",
|
||
}}
|
||
/>
|
||
<header className="relative z-10 border-b border-[#3d3028] bg-[#0c0906]/95 px-4 py-8">
|
||
<div className="mx-auto max-w-3xl">
|
||
<p className="font-mono text-[10px] uppercase tracking-[0.4em] text-[#c45c26]/90">archive</p>
|
||
<h1 className="mt-2 font-mono text-2xl font-black text-[#ffcb8a] md:text-3xl">Hub chatter index</h1>
|
||
<p className="mt-3 max-w-2xl text-sm leading-relaxed text-[#9a8d7c]">
|
||
Scraped-style threads about the <strong className="text-[#ffcb8a]">CyberLux main storefront</strong> — mirror
|
||
checks, checkout banter, UI roasts, class-lab meta. Each row opens in Void Aggregate for full comments.
|
||
</p>
|
||
<nav className="mt-6 flex flex-wrap gap-3 font-mono text-xs">
|
||
<Link
|
||
href="/forum"
|
||
className="rounded border border-[#c45c26] bg-[#c45c26]/15 px-4 py-2 font-bold text-[#ffcb8a] hover:bg-[#c45c26]/25"
|
||
>
|
||
Void Aggregate forum
|
||
</Link>
|
||
<Link href="/" className="rounded border border-[#3d3028] px-4 py-2 text-[#b8a995] hover:border-[#c45c26]/50">
|
||
Main hub
|
||
</Link>
|
||
</nav>
|
||
</div>
|
||
</header>
|
||
|
||
<main className="relative z-10 mx-auto max-w-3xl px-4 pb-24 pt-8">
|
||
<p className="mb-6 font-mono text-xs text-[#6b6358]">
|
||
Showing {(page - 1) * PAGE_SIZE + 1}–{Math.min(page * PAGE_SIZE, total)} of {total} indexed posts ·{" "}
|
||
{PAGE_SIZE} per page
|
||
</p>
|
||
|
||
<ul className="space-y-4">
|
||
{items.map((row) => (
|
||
<li key={row.id} className="rounded-lg border border-[#2a2218] bg-[#0a0705]/95 p-4">
|
||
<div className="flex flex-wrap items-baseline justify-between gap-2 font-mono text-[10px] uppercase tracking-wider text-[#7a6e62]">
|
||
<span className="text-[#ff9a3c]/90">{formatRingLabel(row.topicSlug)}</span>
|
||
<span>
|
||
{row.author} · {formatAgo(row.ts)}
|
||
</span>
|
||
</div>
|
||
<Link
|
||
href={`/forum/post/${row.id}`}
|
||
className="mt-2 block font-mono text-base font-bold leading-snug text-[#ffcb8a] hover:underline"
|
||
>
|
||
{row.title}
|
||
</Link>
|
||
<p className="mt-2 line-clamp-3 text-sm leading-relaxed text-[#b0a090]">{row.body}</p>
|
||
<div className="mt-3 flex flex-wrap gap-3 font-mono text-[11px] text-[#6b6358]">
|
||
<span>{row.replies.length} comments</span>
|
||
<span>score ~{row.baseScore ?? 1}</span>
|
||
<Link href={`/forum/post/${row.id}`} className="text-[#ff9a3c] hover:underline">
|
||
open thread →
|
||
</Link>
|
||
</div>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
|
||
<nav className="mt-12 flex flex-wrap items-center justify-center gap-2 font-mono text-xs">
|
||
{page > 1 ? (
|
||
<Link
|
||
href={`/chatter?page=${page - 1}`}
|
||
className="rounded border border-[#3d3028] px-3 py-2 text-[#ffcb8a] hover:border-[#c45c26]"
|
||
>
|
||
← Newer
|
||
</Link>
|
||
) : (
|
||
<span className="rounded border border-transparent px-3 py-2 text-[#4a433c]">← Newer</span>
|
||
)}
|
||
{pages.map((n) => (
|
||
<Link
|
||
key={n}
|
||
href={`/chatter?page=${n}`}
|
||
className={`rounded border px-3 py-2 ${
|
||
n === page ? "border-[#c45c26] bg-[#c45c26]/20 text-[#ffcb8a]" : "border-[#3d3028] text-[#b8a995] hover:border-[#c45c26]/40"
|
||
}`}
|
||
>
|
||
{n}
|
||
</Link>
|
||
))}
|
||
{page < totalPages ? (
|
||
<Link
|
||
href={`/chatter?page=${page + 1}`}
|
||
className="rounded border border-[#3d3028] px-3 py-2 text-[#ffcb8a] hover:border-[#c45c26]"
|
||
>
|
||
Older →
|
||
</Link>
|
||
) : (
|
||
<span className="rounded border border-transparent px-3 py-2 text-[#4a433c]">Older →</span>
|
||
)}
|
||
</nav>
|
||
|
||
<p className="mt-10 text-center font-mono text-[10px] text-[#4a4338]">Total catalogued: {SHOP_CHATTER_COUNT} threads</p>
|
||
</main>
|
||
</div>
|
||
);
|
||
}
|