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
This commit is contained in:
59
app/exchange/guidelines/page.tsx
Normal file
59
app/exchange/guidelines/page.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import Link from "next/link";
|
||||
|
||||
export default function ExchangeGuidelinesPage() {
|
||||
return (
|
||||
<article className="space-y-8 border-2 border-[#4a3f36] bg-[#12100e] p-6 shadow-[6px_6px_0_#1a1410] md:p-10">
|
||||
<header>
|
||||
<h1 className="text-2xl font-black uppercase text-[#e8ddd0] md:text-3xl">Posting guidelines</h1>
|
||||
<p className="mt-3 text-sm leading-relaxed text-[#8a7b6b]">
|
||||
The Exchange is deliberately lo-fi on purpose: no algorithmic feed, no reputation graph baked into the board itself. These rules describe how CyberLux keeps the classifieds readable without turning them into a half-finished plugin.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section>
|
||||
<h2 className="border-b-2 border-[#3d3228] pb-2 text-sm font-black uppercase text-[#c4a574]">Required fields</h2>
|
||||
<ul className="mt-4 list-disc space-y-2 pl-5 text-sm text-[#b8a99a]">
|
||||
<li>
|
||||
<strong className="text-[#e8ddd0]">Headline</strong> — one line, no all-caps spam; mods may truncate.
|
||||
</li>
|
||||
<li>
|
||||
<strong className="text-[#e8ddd0]">Body</strong> — include contact policy (signed only, dead drop, etc.).
|
||||
</li>
|
||||
<li>
|
||||
<strong className="text-[#e8ddd0]">Price + unit</strong> — numeric field is a label, not an on-chain lock.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="border-b-2 border-[#3d3228] pb-2 text-sm font-black uppercase text-[#c4a574]">Prohibited</h2>
|
||||
<ul className="mt-4 list-disc space-y-2 pl-5 text-sm text-[#b8a99a]">
|
||||
<li>Automated scraping reposts from the Candy Shop catalog — use the market for SKUs.</li>
|
||||
<li>Links to clearnet trackers or JS-heavy paste bins without a plain-text mirror.</li>
|
||||
<li>Impersonation of floor staff; use your dashboard display name consistently.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="border-b-2 border-[#3d3228] pb-2 text-sm font-black uppercase text-[#c4a574]">Dispute stance</h2>
|
||||
<p className="mt-4 text-sm leading-relaxed text-[#8a7b6b]">
|
||||
The board is <em>unmoderated</em> in the sense that listings are not pre-approved. Abuse reports still flow through{" "}
|
||||
<Link href="/support" className="text-[#d4a574] underline hover:text-[#e8c49a]">
|
||||
support
|
||||
</Link>{" "}
|
||||
and may result in account flags that hide your alias from hub widgets — but historical posts remain in localStorage for that browser until cleared.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<p className="text-xs text-[#6b5c4d]">
|
||||
<Link href="/exchange" className="text-[#d4a574] underline">
|
||||
← Back to board
|
||||
</Link>
|
||||
{" · "}
|
||||
<Link href="/exchange/ledger" className="text-[#d4a574] underline">
|
||||
Ledger view
|
||||
</Link>
|
||||
</p>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
5
app/exchange/layout.tsx
Normal file
5
app/exchange/layout.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import ExchangeSiteChrome from "@/components/exchange/ExchangeSiteChrome";
|
||||
|
||||
export default function ExchangeLayout({ children }: { children: React.ReactNode }) {
|
||||
return <ExchangeSiteChrome>{children}</ExchangeSiteChrome>;
|
||||
}
|
||||
93
app/exchange/ledger/page.tsx
Normal file
93
app/exchange/ledger/page.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
172
app/exchange/page.tsx
Normal file
172
app/exchange/page.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
"use client";
|
||||
|
||||
import { FormEvent, useEffect, useMemo, useRef, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useAccount } from "@/contexts/AccountContext";
|
||||
import { addListing, loadExchange, type ExchangeListing } from "@/lib/exchangeState";
|
||||
|
||||
export default function ExchangePlatformPage() {
|
||||
const { user, hydrated: accountReady } = useAccount();
|
||||
const prefilledAuthor = useRef(false);
|
||||
const [rows, setRows] = useState<ExchangeListing[]>([]);
|
||||
const [filter, setFilter] = useState<"all" | "wts" | "wtb">("all");
|
||||
const [kind, setKind] = useState<"wts" | "wtb">("wts");
|
||||
const [title, setTitle] = useState("");
|
||||
const [body, setBody] = useState("");
|
||||
const [price, setPrice] = useState("");
|
||||
const [currency, setCurrency] = useState<"BTC" | "XMR" | "USD">("BTC");
|
||||
const [author, setAuthor] = useState("");
|
||||
const [exchangeReady, setExchangeReady] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setRows(loadExchange());
|
||||
setExchangeReady(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!accountReady || !user || prefilledAuthor.current) return;
|
||||
setAuthor(user.displayName);
|
||||
prefilledAuthor.current = true;
|
||||
}, [accountReady, user]);
|
||||
|
||||
const list = useMemo(() => {
|
||||
if (filter === "all") return rows;
|
||||
return rows.filter((r) => r.kind === filter);
|
||||
}, [rows, filter]);
|
||||
|
||||
const onSubmit = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!title.trim() || !body.trim() || !price.trim()) return;
|
||||
addListing({
|
||||
kind,
|
||||
title,
|
||||
body,
|
||||
price,
|
||||
currency,
|
||||
author,
|
||||
});
|
||||
setRows(loadExchange());
|
||||
setTitle("");
|
||||
setBody("");
|
||||
setPrice("");
|
||||
};
|
||||
|
||||
const btn = (active: boolean) =>
|
||||
`flex-1 border-2 border-[#6b5344] py-3 text-xs font-black uppercase transition-colors ${
|
||||
active ? "bg-[#8d6e4a]/40 text-[#f0e6d8]" : "bg-transparent text-[#a89888] hover:bg-[#1a1614]"
|
||||
}`;
|
||||
|
||||
const tab = (active: boolean) =>
|
||||
`border-2 px-4 py-2 text-xs font-black uppercase ${
|
||||
active ? "border-[#c4a574] bg-[#c4a574]/20 text-[#f0e6d8]" : "border-[#4a3f36]/80 bg-transparent text-[#8a7b6b]"
|
||||
}`;
|
||||
|
||||
return (
|
||||
<main className="mx-auto max-w-5xl pb-8 pt-2">
|
||||
<p className="mb-6 text-center text-[11px] text-[#6b5c4d]">
|
||||
<Link href="/exchange/guidelines" className="text-[#c4a574] underline hover:text-[#e8c49a]">
|
||||
Guidelines
|
||||
</Link>
|
||||
<span className="mx-2 text-[#3d3228]">·</span>
|
||||
<Link href="/exchange/ledger" className="text-[#c4a574] underline hover:text-[#e8c49a]">
|
||||
Ledger
|
||||
</Link>
|
||||
<span className="mx-2 text-[#3d3228]">·</span>
|
||||
<Link href="/sign-up" className="text-[#c4a574] underline hover:text-[#e8c49a]">
|
||||
Sign up
|
||||
</Link>
|
||||
</p>
|
||||
<section className="border-2 border-[#4a3f36] bg-[#12100e] p-6 shadow-[6px_6px_0_#1a1410] md:p-8">
|
||||
<h2 className="border-b-2 border-[#3d3228] pb-2 text-lg font-black uppercase text-[#e8ddd0]">Submit a listing</h2>
|
||||
<form onSubmit={onSubmit} className="mt-6 grid gap-4 md:grid-cols-2">
|
||||
<div className="flex gap-2 md:col-span-2">
|
||||
<button type="button" onClick={() => setKind("wts")} className={btn(kind === "wts")}>
|
||||
WTS — selling
|
||||
</button>
|
||||
<button type="button" onClick={() => setKind("wtb")} className={btn(kind === "wtb")}>
|
||||
WTB — buying
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
className="border-2 border-[#4a3f36] bg-[#1a1612] px-4 py-3 text-sm text-[#e8ddd0] placeholder:text-[#5c5048] focus:outline-none focus:ring-2 focus:ring-[#8d6e4a]/50"
|
||||
placeholder="Alias"
|
||||
value={author}
|
||||
onChange={(e) => setAuthor(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
className="border-2 border-[#4a3f36] bg-[#1a1612] px-4 py-3 text-sm text-[#e8ddd0] placeholder:text-[#5c5048] focus:outline-none focus:ring-2 focus:ring-[#8d6e4a]/50"
|
||||
placeholder="Headline"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
className="border-2 border-[#4a3f36] bg-[#1a1612] px-4 py-3 text-sm text-[#e8ddd0] placeholder:text-[#5c5048] focus:outline-none focus:ring-2 focus:ring-[#8d6e4a]/50"
|
||||
placeholder="Price (number)"
|
||||
value={price}
|
||||
onChange={(e) => setPrice(e.target.value)}
|
||||
/>
|
||||
<select
|
||||
className="border-2 border-[#4a3f36] bg-[#1a1612] px-4 py-3 text-sm text-[#e8ddd0] focus:outline-none focus:ring-2 focus:ring-[#8d6e4a]/50"
|
||||
value={currency}
|
||||
onChange={(e) => setCurrency(e.target.value as "BTC" | "XMR" | "USD")}
|
||||
>
|
||||
<option value="BTC">BTC</option>
|
||||
<option value="XMR">XMR</option>
|
||||
<option value="USD">USD</option>
|
||||
</select>
|
||||
<textarea
|
||||
className="min-h-[120px] border-2 border-[#4a3f36] bg-[#1a1612] px-4 py-3 text-sm text-[#e8ddd0] placeholder:text-[#5c5048] md:col-span-2 focus:outline-none focus:ring-2 focus:ring-[#8d6e4a]/50"
|
||||
placeholder="Terms, shipping, contact policy, optional PGP block…"
|
||||
value={body}
|
||||
onChange={(e) => setBody(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="md:col-span-2 border-2 border-[#8d6e4a] bg-[#8d6e4a]/25 py-4 text-sm font-black uppercase tracking-wide text-[#f0e6d8] hover:bg-[#8d6e4a]/40"
|
||||
>
|
||||
Print to board
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<div className="mb-6 mt-10 flex gap-2">
|
||||
{(
|
||||
[
|
||||
["all", "All"],
|
||||
["wts", "WTS"],
|
||||
["wtb", "WTB"],
|
||||
] as const
|
||||
).map(([k, label]) => (
|
||||
<button key={k} type="button" onClick={() => setFilter(k)} className={tab(filter === k)}>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="columns-1 gap-6 md:columns-2">
|
||||
{!exchangeReady ? (
|
||||
<p className="text-sm text-[#8a7b6b]">Loading…</p>
|
||||
) : (
|
||||
list.map((r) => (
|
||||
<article
|
||||
key={r.id}
|
||||
className="mb-6 break-inside-avoid border-2 border-[#4a3f36] bg-[#12100e] p-5 shadow-[4px_4px_0_#1a1410]"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2 border-b border-[#3d3228] pb-2">
|
||||
<span className="text-[10px] font-black uppercase tracking-widest text-[#c4a574]">{r.kind}</span>
|
||||
<time className="text-[10px] text-[#8a7b6b]">{new Date(r.ts).toLocaleString()}</time>
|
||||
</div>
|
||||
<h3 className="mt-3 text-xl font-black leading-tight text-[#f0e6d8]">{r.title}</h3>
|
||||
<p className="mt-2 text-sm">
|
||||
<span className="font-bold text-[#d6cbb8]">{r.author}</span>
|
||||
<span className="mx-1 text-[#6b5c4d]">—</span>
|
||||
<span className="font-mono text-sm font-bold text-[#e8c49a]">{r.price}</span> {r.currency}
|
||||
</p>
|
||||
<p className="mt-4 whitespace-pre-wrap text-sm leading-relaxed text-[#b8a99a]">{r.body}</p>
|
||||
</article>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user