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
210 lines
9.9 KiB
TypeScript
210 lines
9.9 KiB
TypeScript
"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 48–72h 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 you’ll 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>
|
||
);
|
||
}
|