Files
dark-lord/app/barter/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

185 lines
8.0 KiB
TypeScript

"use client";
import { FormEvent, useEffect, useMemo, useRef, useState } from "react";
import { useSearchParams } from "next/navigation";
import { useAccount } from "@/contexts/AccountContext";
import { addBarterListing, loadBarter, type BarterLane, type BarterListing } from "@/lib/barterState";
const LANES: { id: BarterLane; label: string; hint: string }[] = [
{ id: "goods", label: "Physical / goods", hint: "Hardware, prints, tokens, boxes…" },
{ id: "services", label: "Labor / skills", hint: "Tune-ups, consults, studio time…" },
{ id: "data", label: "Data / media", hint: "Scans, decks, anonymized sets…" },
{ id: "open", label: "Open terms", hint: "Wildcard swaps — spell it out." },
];
export default function BarterPitPage() {
const sp = useSearchParams();
const laneFilter = sp.get("lane") as BarterLane | null;
const { user, hydrated: accountReady } = useAccount();
const prefilledAuthor = useRef(false);
const [rows, setRows] = useState<BarterListing[]>([]);
const [barterReady, setBarterReady] = useState(false);
const [lane, setLane] = useState<BarterLane>("open");
const [author, setAuthor] = useState("");
const [title, setTitle] = useState("");
const [have, setHave] = useState("");
const [want, setWant] = useState("");
const [body, setBody] = useState("");
useEffect(() => {
setRows(loadBarter());
setBarterReady(true);
}, []);
useEffect(() => {
if (!accountReady || !user || prefilledAuthor.current) return;
setAuthor(user.displayName);
prefilledAuthor.current = true;
}, [accountReady, user]);
useEffect(() => {
if (laneFilter && ["goods", "services", "data", "open"].includes(laneFilter)) {
setLane(laneFilter);
}
}, [laneFilter]);
const filtered = useMemo(() => {
const sorted = [...rows].sort((a, b) => b.ts - a.ts);
if (!laneFilter || !["goods", "services", "data", "open"].includes(laneFilter)) return sorted;
return sorted.filter((r) => r.lane === laneFilter);
}, [rows, laneFilter]);
const onSubmit = (e: FormEvent) => {
e.preventDefault();
if (!title.trim() || !have.trim() || !want.trim()) return;
addBarterListing({
lane,
author,
title,
have,
want,
body,
});
setRows(loadBarter());
setTitle("");
setHave("");
setWant("");
setBody("");
};
return (
<div className="grid gap-10 lg:grid-cols-[1fr_380px] lg:items-start">
<section className="space-y-4">
{!barterReady ? (
<p className="font-mono text-sm text-[#5a7d62]">Syncing order book</p>
) : (
filtered.map((r) => (
<article
key={r.id}
className="rounded-lg border border-[#164024] bg-[#070d0a]/90 p-5 shadow-[0_0_0_1px_rgba(57,255,20,0.06)]"
>
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-[#0d2818] pb-3 font-mono text-[10px] uppercase tracking-widest text-[#39ff14]/65">
<span>{r.lane}</span>
<time className="text-[#5a7d62]">{new Date(r.ts).toLocaleString()}</time>
</div>
<h2 className="mt-3 font-mono text-lg font-bold text-[#c8ffce]">{r.title}</h2>
<p className="mt-1 font-mono text-[11px] text-[#6b9b78]">
<span className="text-[#39ff14]/80">{r.author || "Anonymous"}</span>
</p>
<div className="mt-4 grid gap-3 font-mono text-sm md:grid-cols-2">
<div className="rounded border border-[#123218] bg-[#050a07] p-3">
<p className="text-[9px] uppercase tracking-[0.2em] text-[#39ff14]/55">Offers (have)</p>
<p className="mt-2 leading-relaxed text-[#b8f5c6]">{r.have}</p>
</div>
<div className="rounded border border-[#123218] bg-[#050a07] p-3">
<p className="text-[9px] uppercase tracking-[0.2em] text-[#39ff14]/55">Wants (take)</p>
<p className="mt-2 leading-relaxed text-[#b8f5c6]">{r.want}</p>
</div>
</div>
{r.body ? (
<p className="mt-4 whitespace-pre-wrap font-mono text-xs leading-relaxed text-[#7aaf88]/95">{r.body}</p>
) : null}
</article>
))
)}
</section>
<aside className="lg:sticky lg:top-28">
<div className="rounded-lg border border-[#39ff14]/25 bg-[#050a07]/95 p-5">
<h3 className="font-mono text-xs font-bold uppercase tracking-[0.25em] text-[#39ff14]/85">Post a swap</h3>
<p className="mt-2 font-mono text-[10px] leading-relaxed text-[#5a7d62]">
Barter only no automated matching. Treat every DM like a phishing drill until PGP proves otherwise.
</p>
<form onSubmit={onSubmit} className="mt-5 space-y-4">
<div>
<label className="font-mono text-[9px] uppercase tracking-wider text-[#6b9b78]">Lane</label>
<select
value={lane}
onChange={(e) => setLane(e.target.value as BarterLane)}
className="mt-1 w-full rounded border border-[#164024] bg-[#030806] px-3 py-2 font-mono text-xs text-[#b8f5c6] focus:border-[#39ff14]/50 focus:outline-none"
>
{LANES.map((l) => (
<option key={l.id} value={l.id} className="bg-[#030806]">
{l.label}
</option>
))}
</select>
</div>
<input
className="w-full rounded border border-[#164024] bg-[#030806] px-3 py-2 font-mono text-xs text-[#b8f5c6] placeholder:text-[#3d5244] focus:border-[#39ff14]/45 focus:outline-none"
placeholder="Alias"
value={author}
onChange={(e) => setAuthor(e.target.value)}
/>
<input
required
className="w-full rounded border border-[#164024] bg-[#030806] px-3 py-2 font-mono text-xs text-[#b8f5c6] placeholder:text-[#3d5244] focus:border-[#39ff14]/45 focus:outline-none"
placeholder="Headline (short)"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<textarea
required
rows={3}
className="w-full rounded border border-[#164024] bg-[#030806] px-3 py-2 font-mono text-xs text-[#b8f5c6] placeholder:text-[#3d5244] focus:border-[#39ff14]/45 focus:outline-none"
placeholder="What you're putting on the table…"
value={have}
onChange={(e) => setHave(e.target.value)}
/>
<textarea
required
rows={3}
className="w-full rounded border border-[#164024] bg-[#030806] px-3 py-2 font-mono text-xs text-[#b8f5c6] placeholder:text-[#3d5244] focus:border-[#39ff14]/45 focus:outline-none"
placeholder="What you want back (goods, hours, XMR, favor…)…"
value={want}
onChange={(e) => setWant(e.target.value)}
/>
<textarea
rows={4}
className="w-full rounded border border-[#164024] bg-[#030806] px-3 py-2 font-mono text-xs text-[#b8f5c6] placeholder:text-[#3d5244] focus:border-[#39ff14]/45 focus:outline-none"
placeholder="Terms, meetup fiction, PGP fingerprint, escrow notes…"
value={body}
onChange={(e) => setBody(e.target.value)}
/>
<button
type="submit"
className="w-full rounded border border-[#39ff14] bg-[#39ff14]/15 py-3 font-mono text-xs font-black uppercase tracking-wider text-[#c8ffce] hover:bg-[#39ff14]/25"
>
List on floor
</button>
</form>
<ul className="mt-5 space-y-2 border-t border-[#0d2818] pt-4 font-mono text-[10px] text-[#5a7d62]">
{LANES.map((l) => (
<li key={l.id}>
<span className="text-[#39ff14]/55">{l.label}:</span> {l.hint}
</li>
))}
</ul>
</div>
</aside>
</div>
);
}