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
This commit is contained in:
184
app/barter/page.tsx
Normal file
184
app/barter/page.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user