Files
dark-lord/app/barter/page.tsx
2026-04-26 22:28:40 -07:00

193 lines
8.2 KiB
TypeScript

"use client";
import { FormEvent, Suspense, 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." },
];
function BarterPitContent() {
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>
);
}
export default function BarterPitPage() {
return (
<Suspense fallback={<p className="font-mono text-sm text-[#5a7d62]">Syncing order book</p>}>
<BarterPitContent />
</Suspense>
);
}