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:
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