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
94 lines
3.8 KiB
TypeScript
94 lines
3.8 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import Link from "next/link";
|
|
import { loadExchange, type ExchangeListing } from "@/lib/exchangeState";
|
|
|
|
export default function ExchangeLedgerPage() {
|
|
const [rows, setRows] = useState<ExchangeListing[]>([]);
|
|
const [ready, setReady] = useState(false);
|
|
|
|
useEffect(() => {
|
|
setRows(loadExchange());
|
|
setReady(true);
|
|
}, []);
|
|
|
|
const stats = useMemo(() => {
|
|
const wts = rows.filter((r) => r.kind === "wts").length;
|
|
const wtb = rows.filter((r) => r.kind === "wtb").length;
|
|
const byCur = { BTC: 0, XMR: 0, USD: 0 } as Record<string, number>;
|
|
for (const r of rows) {
|
|
byCur[r.currency] = (byCur[r.currency] ?? 0) + 1;
|
|
}
|
|
const newest = rows.length ? Math.max(...rows.map((r) => r.ts)) : 0;
|
|
const oldest = rows.length ? Math.min(...rows.map((r) => r.ts)) : 0;
|
|
return { wts, wtb, byCur, newest, oldest, n: rows.length };
|
|
}, [rows]);
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
<header className="border-2 border-[#4a3f36] bg-[#12100e] p-6 md:p-8">
|
|
<h1 className="text-2xl font-black uppercase text-[#e8ddd0]">Ledger view</h1>
|
|
<p className="mt-2 text-sm text-[#8a7b6b]">
|
|
Aggregates whatever is stored under <code className="text-[#c4a574]">cyberlux-exchange-v1</code> in this browser. No external analytics SDK — just local state, same source the board reads.
|
|
</p>
|
|
</header>
|
|
|
|
{!ready ? (
|
|
<p className="text-sm text-[#8a7b6b]">Loading ledger…</p>
|
|
) : (
|
|
<>
|
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
<div className="border-2 border-[#4a3f36] bg-[#12100e] p-4">
|
|
<p className="text-[10px] font-black uppercase text-[#6b5c4d]">Total rows</p>
|
|
<p className="mt-1 text-3xl font-black text-[#f0e6d8]">{stats.n}</p>
|
|
</div>
|
|
<div className="border-2 border-[#4a3f36] bg-[#12100e] p-4">
|
|
<p className="text-[10px] font-black uppercase text-[#6b5c4d]">WTS</p>
|
|
<p className="mt-1 text-3xl font-black text-[#c4a574]">{stats.wts}</p>
|
|
</div>
|
|
<div className="border-2 border-[#4a3f36] bg-[#12100e] p-4">
|
|
<p className="text-[10px] font-black uppercase text-[#6b5c4d]">WTB</p>
|
|
<p className="mt-1 text-3xl font-black text-[#8d6e4a]">{stats.wtb}</p>
|
|
</div>
|
|
<div className="border-2 border-[#4a3f36] bg-[#12100e] p-4">
|
|
<p className="text-[10px] font-black uppercase text-[#6b5c4d]">Newest post</p>
|
|
<p className="mt-1 font-mono text-sm text-[#e8ddd0]">
|
|
{stats.newest ? new Date(stats.newest).toLocaleString() : "—"}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<section className="border-2 border-[#4a3f36] bg-[#12100e] p-6">
|
|
<h2 className="text-sm font-black uppercase text-[#c4a574]">Currency tags</h2>
|
|
<ul className="mt-4 space-y-2 font-mono text-xs text-[#b8a99a]">
|
|
{(["BTC", "XMR", "USD"] as const).map((c) => (
|
|
<li key={c} className="flex justify-between border-b border-[#2a2218] py-2">
|
|
<span>{c}</span>
|
|
<span className="text-[#e8c49a]">{stats.byCur[c] ?? 0}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</section>
|
|
|
|
{stats.oldest && stats.newest ? (
|
|
<p className="text-xs text-[#6b5c4d]">
|
|
Span: {new Date(stats.oldest).toLocaleDateString()} — {new Date(stats.newest).toLocaleDateString()}
|
|
</p>
|
|
) : null}
|
|
</>
|
|
)}
|
|
|
|
<p className="text-xs">
|
|
<Link href="/exchange" className="text-[#d4a574] underline">
|
|
Board
|
|
</Link>
|
|
{" · "}
|
|
<Link href="/exchange/guidelines" className="text-[#d4a574] underline">
|
|
Guidelines
|
|
</Link>
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|