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
173 lines
7.2 KiB
TypeScript
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>
|
|
);
|
|
}
|