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
185 lines
8.0 KiB
TypeScript
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>
|
|
);
|
|
}
|