Files
dark-lord/app/exchange/page.tsx
drjones 78a071ba02 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
2026-04-07 21:35:52 -07:00

173 lines
7.2 KiB
TypeScript

"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>
);
}