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:
drjones
2026-04-07 21:35:52 -07:00
parent 52432dccfa
commit 78a071ba02
162 changed files with 21692 additions and 39 deletions

78
app/vendor/[slug]/page.tsx vendored Normal file
View File

@@ -0,0 +1,78 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import { VendorShelfRow } from "@/components/shop/VendorShelfRow";
import { shopProductsForSeller } from "@/lib/shopCatalog";
import { SHOP_SELLERS, getSellerBySlug } from "@/lib/shopSellers";
export function generateStaticParams() {
return SHOP_SELLERS.map((s) => ({ slug: s.slug }));
}
type Props = { params: Promise<{ slug: string }> };
export default async function VendorProfilePage({ params }: Props) {
const { slug } = await params;
const v = getSellerBySlug(slug);
if (!v) notFound();
const stock = shopProductsForSeller(v.id);
return (
<div className="min-h-screen bg-[#050302] text-[#f5f0e8]">
<header className="border-b border-orange-950/60 bg-[#0a0705]">
<div className="mx-auto max-w-4xl px-4 py-10">
<nav className="mb-6 flex flex-wrap gap-2 font-mono text-[11px] text-orange-300/60">
<Link href="/vendors" className="hover:text-orange-100">
Vendor hall
</Link>
<span className="text-orange-900">/</span>
<Link href="/market" className="hover:text-orange-100">
Catalog
</Link>
<span className="text-orange-900">/</span>
<Link href="/" className="hover:text-orange-100">
Hub
</Link>
</nav>
<div className="flex flex-wrap items-baseline gap-3">
<h1 className="font-orbitron text-3xl font-black text-orange-50 md:text-4xl">{v.handle}</h1>
<span className="rounded-full border border-orange-500/40 bg-orange-500/10 px-3 py-0.5 font-mono text-xs text-orange-200">
since {v.since}
</span>
</div>
<p className="mt-2 font-mono text-sm text-orange-400/90">{v.tagline}</p>
<p className="mt-4 max-w-2xl text-sm leading-relaxed text-orange-100/75">{v.bio}</p>
<dl className="mt-6 grid gap-3 font-mono text-xs sm:grid-cols-4">
<div className="rounded border border-orange-900/40 bg-black/30 px-3 py-2">
<dt className="text-orange-500/60">Rating</dt>
<dd className="text-orange-100"> {v.rating.toFixed(2)}</dd>
</div>
<div className="rounded border border-orange-900/40 bg-black/30 px-3 py-2">
<dt className="text-orange-500/60">Lore sales</dt>
<dd className="text-orange-100">{v.completedSales.toLocaleString()}</dd>
</div>
<div className="rounded border border-orange-900/40 bg-black/30 px-3 py-2">
<dt className="text-orange-500/60">Typical reply</dt>
<dd className="text-orange-100">~{v.responseMins}m</dd>
</div>
<div className="rounded border border-orange-900/40 bg-black/30 px-3 py-2">
<dt className="text-orange-500/60">Focus</dt>
<dd className="text-orange-100">{v.specialty}</dd>
</div>
</dl>
</div>
</header>
<main className="mx-auto max-w-4xl px-4 py-10">
<h2 className="font-mono text-[11px] font-bold uppercase tracking-[0.3em] text-orange-500/70">
Listings on this stall · {stock.length}
</h2>
<ul className="mt-6 space-y-4">
{stock.map((p) => (
<VendorShelfRow key={p.id} product={p} />
))}
</ul>
</main>
</div>
);
}

209
app/vendor/apply/page.tsx vendored Normal file
View File

@@ -0,0 +1,209 @@
"use client";
import { FormEvent, useEffect, useState } from "react";
import Link from "next/link";
import { useAccount } from "@/contexts/AccountContext";
import { shopCategoryCounts } from "@/lib/shopCatalog";
import { appendVendorApplication } from "@/lib/vendorApplicationState";
const OTHER = "Other / mixed";
export default function VendorApplyPage() {
const { user, hydrated } = useAccount();
const [categories, setCategories] = useState<{ name: string; icon: string }[]>([]);
const [desiredHandle, setDesiredHandle] = useState("");
const [stallName, setStallName] = useState("");
const [specialtyCategory, setSpecialtyCategory] = useState("");
const [pitch, setPitch] = useState("");
const [pgpFingerprint, setPgpFingerprint] = useState("");
const [deadDropNotes, setDeadDropNotes] = useState("");
const [accepted, setAccepted] = useState(false);
const [done, setDone] = useState(false);
const [refId, setRefId] = useState<string | null>(null);
const [err, setErr] = useState<string | null>(null);
useEffect(() => {
setCategories(shopCategoryCounts().map(({ name, icon }) => ({ name, icon })));
}, []);
useEffect(() => {
if (hydrated && user && !stallName) {
setStallName(`${user.displayName} stall`);
setDesiredHandle((h) => h || user.username);
}
}, [hydrated, user, stallName]);
const onSubmit = (e: FormEvent) => {
e.preventDefault();
setErr(null);
const h = desiredHandle.trim();
if (h.length < 3 || !/^[a-zA-Z0-9_]+$/.test(h)) {
setErr("Desired handle: 3+ chars, letters, numbers, underscores only.");
return;
}
if (!stallName.trim() || pitch.trim().length < 40) {
setErr("Stall name required; pitch must be at least 40 characters — describe stock, bond, and delivery lane.");
return;
}
if (!specialtyCategory) {
setErr("Pick a specialty aisle.");
return;
}
if (!accepted) {
setErr("Confirm vendor policy attestation — check the box.");
return;
}
const saved = appendVendorApplication({
accountUsername: user?.username ?? null,
desiredHandle: h,
stallName: stallName.trim(),
specialtyCategory,
pitch: pitch.trim(),
pgpFingerprint: pgpFingerprint.trim(),
deadDropNotes: deadDropNotes.trim(),
acceptedSimulatorTerms: true,
});
setRefId(saved.id);
setDone(true);
};
return (
<div className="min-h-screen bg-[#030805] text-[#dce8df]">
<header className="border-b border-emerald-900/50 bg-[#050a08]">
<div className="mx-auto max-w-2xl px-4 py-10">
<nav className="mb-6 flex flex-wrap gap-2 font-mono text-[11px] text-emerald-500/70">
<Link href="/vendors" className="hover:text-emerald-200">
Vendor hall
</Link>
<span className="text-emerald-900">/</span>
<Link href="/market" className="hover:text-emerald-200">
Catalog
</Link>
</nav>
<p className="font-mono text-[10px] uppercase tracking-[0.4em] text-emerald-500/60">vendor intake · encrypted queue</p>
<h1 className="mt-2 font-orbitron text-3xl font-black text-emerald-100 md:text-4xl">Apply to vend</h1>
<p className="mt-3 text-sm leading-relaxed text-emerald-200/65">
Bond review opens after you file this form. Submissions are encrypted client-side and queued for moderator triage expect
PGP-signed follow-up within 4872h on business mirrors. Scam lanes get burned; do not paste live customer data into fields
unless you are comfortable with archival retention on this host.
</p>
</div>
</header>
<main className="mx-auto max-w-2xl px-4 py-10">
{done ? (
<div className="rounded-xl border border-emerald-500/40 bg-emerald-950/40 p-8">
<p className="font-mono text-sm font-bold text-emerald-300">Application accepted · pending review</p>
<p className="mt-3 text-sm text-emerald-100/80">
Reference id: <code className="rounded bg-black/40 px-2 py-0.5 font-mono text-xs text-emerald-400">{refId}</code>
</p>
<p className="mt-4 text-xs text-emerald-500/70">
Keep this reference id for bond wire and mirror signing. If you lose session access before approval, re-file with the same
PGP fingerprint and ops will merge threads.
</p>
<div className="mt-8 flex flex-wrap gap-3 text-xs">
<Link href="/vendors" className="rounded border border-emerald-600/50 px-4 py-2 text-emerald-200 hover:bg-emerald-900/30">
Back to vendor hall
</Link>
<Link href="/market" className="rounded border border-emerald-600/50 px-4 py-2 text-emerald-200 hover:bg-emerald-900/30">
Browse catalog
</Link>
</div>
</div>
) : (
<form onSubmit={onSubmit} className="space-y-6 rounded-xl border border-emerald-900/40 bg-[#060c09]/90 p-6 md:p-8">
{err ? (
<p className="rounded-lg border border-red-500/40 bg-red-950/50 px-4 py-2 text-sm text-red-200">{err}</p>
) : null}
<div>
<label className="block font-mono text-[10px] uppercase tracking-wider text-emerald-500/80">Desired handle</label>
<input
value={desiredHandle}
onChange={(e) => setDesiredHandle(e.target.value)}
className="mt-2 w-full border border-emerald-900/50 bg-black/50 px-4 py-3 font-mono text-sm text-emerald-100 placeholder:text-emerald-800 focus:border-emerald-500/60 focus:outline-none"
placeholder="ledger_rat_2"
autoComplete="off"
/>
</div>
<div>
<label className="block font-mono text-[10px] uppercase tracking-wider text-emerald-500/80">Stall / shop name</label>
<input
value={stallName}
onChange={(e) => setStallName(e.target.value)}
className="mt-2 w-full border border-emerald-900/50 bg-black/50 px-4 py-3 font-mono text-sm text-emerald-100 placeholder:text-emerald-800 focus:border-emerald-500/60 focus:outline-none"
placeholder="Midnight solder circus"
/>
</div>
<div>
<label className="block font-mono text-[10px] uppercase tracking-wider text-emerald-500/80">Specialty aisle</label>
<select
value={specialtyCategory}
onChange={(e) => setSpecialtyCategory(e.target.value)}
className="mt-2 w-full border border-emerald-900/50 bg-black/50 px-4 py-3 font-mono text-sm text-emerald-100 focus:border-emerald-500/60 focus:outline-none"
>
<option value=""> choose </option>
{categories.map((c) => (
<option key={c.name} value={c.name}>
{c.icon} {c.name}
</option>
))}
<option value={OTHER}>{OTHER}</option>
</select>
</div>
<div>
<label className="block font-mono text-[10px] uppercase tracking-wider text-emerald-500/80">Elevator pitch (min 40 chars)</label>
<textarea
value={pitch}
onChange={(e) => setPitch(e.target.value)}
rows={6}
className="mt-2 w-full border border-emerald-900/50 bg-black/50 px-4 py-3 font-mono text-sm text-emerald-100 placeholder:text-emerald-800 focus:border-emerald-500/60 focus:outline-none"
placeholder="SKUs youll float, sourcing story, buyer verification flow, expected ship regions, dispute stance…"
/>
<p className="mt-1 font-mono text-[10px] text-emerald-600/70">{pitch.trim().length} / 40+</p>
</div>
<div className="grid gap-6 md:grid-cols-2">
<div>
<label className="block font-mono text-[10px] uppercase tracking-wider text-emerald-500/80">PGP fingerprint (optional)</label>
<input
value={pgpFingerprint}
onChange={(e) => setPgpFingerprint(e.target.value)}
className="mt-2 w-full border border-emerald-900/50 bg-black/50 px-4 py-3 font-mono text-xs text-emerald-100 focus:border-emerald-500/60 focus:outline-none"
placeholder="12AB … full 40-char fingerprint"
/>
</div>
<div>
<label className="block font-mono text-[10px] uppercase tracking-wider text-emerald-500/80">Secure contact channel</label>
<input
value={deadDropNotes}
onChange={(e) => setDeadDropNotes(e.target.value)}
className="mt-2 w-full border border-emerald-900/50 bg-black/50 px-4 py-3 font-mono text-xs text-emerald-100 focus:border-emerald-500/60 focus:outline-none"
placeholder="Session-only Jabber, alt mailbox, or dead-drop rotation code"
/>
</div>
</div>
<label className="flex cursor-pointer items-start gap-3 text-sm text-emerald-200/85">
<input type="checkbox" checked={accepted} onChange={(e) => setAccepted(e.target.checked)} className="mt-1" />
<span>
I accept vendor bond rules, mirror escrow, and sanctions screening for this marketplace. I will not paste clearnet PII or
jurisdictional evidence into fields without redaction.
</span>
</label>
<button
type="submit"
className="w-full rounded-lg border-2 border-emerald-500 bg-emerald-600/90 py-4 font-orbitron text-sm font-bold uppercase tracking-wider text-black hover:bg-emerald-400"
>
Submit application
</button>
</form>
)}
</main>
</div>
);
}