Update market, account, and onion operations

Capture the current CyberLux UI, commerce, messaging, and Tor ops updates so local main can be pushed to the remote.

Made-with: Cursor
This commit is contained in:
drjones
2026-04-24 23:23:48 -07:00
parent cb072437d0
commit 04d64fb993
44 changed files with 2256 additions and 645 deletions

View File

@@ -7,130 +7,72 @@ import { useAccount } from "@/contexts/AccountContext";
type Msg = { id: string; from: string; text: string; ts: number };
const MSG_KEY_PREFIX = "cyberlux-msgs-v1";
const BOT_NAMES = ["void_cartographer", "relay_op", "ledger_moth", "phantom_q", "EU_shift"];
const BOT_REPLIES: Record<string, string[]> = {
market: [
"Check /market — catalog updates every ~6h.",
"New drops typically show up late cycle. /drops has the schedule.",
],
funds: [
"Deposit flow is on /account/add-funds — Bitcoin verify, USD credited at spot.",
"After one confirmation paste your txid at /account/add-funds. Done.",
],
forum: [
"Forum is at /forum — threaded, ring-gated, persists per device.",
"Post your thread on /forum/submit if you want a dedicated slot.",
],
exchange: [
"Classifieds are live on /exchange — WTS/WTB, stored local.",
"Exchange listings open to any signed-in handle.",
],
barter: ["Ash Pit (/barter) is the swap board. Four lanes: goods, services, data, open."],
default: [
"Copy that.",
"Noted.",
"Channel is live.",
"Acknowledged.",
"Check your vault for anything pending.",
"Markets move. Stay verified.",
],
};
function getBotReply(text: string): string {
const t = text.toLowerCase();
for (const [k, v] of Object.entries(BOT_REPLIES)) {
if (k !== "default" && t.includes(k)) {
return v[Math.floor(Math.random() * v.length)]!;
}
}
return BOT_REPLIES.default[Math.floor(Math.random() * BOT_REPLIES.default.length)]!;
}
function uid() {
return Math.random().toString(36).slice(2, 10);
}
function botName() {
return BOT_NAMES[Math.floor(Math.random() * BOT_NAMES.length)]!;
}
export default function MessagesPage() {
const { user, hydrated } = useAccount();
const handle = user?.username ?? null;
const storageKey = `${MSG_KEY_PREFIX}:${handle ?? "anon"}`;
const [msgs, setMsgs] = useState<Msg[]>([]);
const [input, setInput] = useState("");
const [ready, setReady] = useState(false);
const endRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!hydrated) return;
const fetchMessages = useCallback(async () => {
try {
const raw = localStorage.getItem(storageKey);
if (raw) {
setMsgs(JSON.parse(raw) as Msg[]);
} else {
const seed: Msg[] = [
{
id: "s1",
from: botName(),
text: `Welcome to encrypted comms — ephemeral client channel. Posts here are stored only on your device under key "${storageKey}". Sign in to separate conversations per handle.`,
ts: Date.now() - 180000,
},
];
setMsgs(seed);
localStorage.setItem(storageKey, JSON.stringify(seed));
const res = await fetch("/api/messages?channel=global");
const data = await res.json();
if (data.ok && data.messages) {
setMsgs(data.messages);
}
} catch {
setReady(true);
} catch (e) {
// ignore
}
setReady(true);
}, [storageKey, hydrated]);
}, []);
useEffect(() => {
if (!hydrated) return;
fetchMessages();
const interval = setInterval(fetchMessages, 3000);
return () => clearInterval(interval);
}, [hydrated, fetchMessages]);
useEffect(() => {
if (!ready) return;
try {
localStorage.setItem(storageKey, JSON.stringify(msgs.slice(-100)));
} catch {
// ignore
}
endRef.current?.scrollIntoView({ behavior: "smooth" });
}, [msgs, ready, storageKey]);
}, [msgs, ready]);
const send = useCallback(() => {
const send = async () => {
const text = input.trim();
if (!text) return;
const mine: Msg = { id: uid(), from: handle ?? "anon", text, ts: Date.now() };
setMsgs((p) => [...p, mine]);
setInput("");
const delay = 700 + Math.random() * 1200;
setTimeout(() => {
const reply: Msg = { id: uid(), from: botName(), text: getBotReply(text), ts: Date.now() };
setMsgs((p) => [...p, reply]);
}, delay);
}, [input, handle]);
const clearHistory = () => {
setMsgs([]);
localStorage.removeItem(storageKey);
const optimisticMsg: Msg = { id: "temp-" + Date.now(), from: handle ?? "anon", text, ts: Date.now() };
setMsgs((p) => [...p, optimisticMsg]);
try {
await fetch("/api/messages", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ from: handle ?? "anon", text, channel: "global" }),
});
fetchMessages();
} catch (e) {
// ignore
}
};
const ts = (n: number) =>
new Date(n).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
return (
<ThemedLayout theme="terminal" siteTitle="ENCRYPTED COMMS" siteSubtitle="Per-handle ephemeral channel">
<Sections>
<section className="container mx-auto max-w-2xl px-4 py-12">
<div className="mb-6 flex flex-wrap items-center justify-between gap-3">
<div>
<h1 className="font-orbitron text-2xl font-bold theme-accent">ENCRYPTED COMMS</h1>
<h1 className="font-orbitron text-2xl font-bold theme-accent">GLOBAL COMMS</h1>
<p className="mt-1 text-xs text-foreground/50">
{handle ? (
<>Channel: <span className="text-neon-cyan">@{handle}</span> · stored locally · no server transport</>
<>Channel: <span className="text-neon-cyan">@{handle}</span> · synced across network</>
) : (
<>
<Link href="/sign-in?next=/messages" className="text-neon-cyan underline">
@@ -141,19 +83,14 @@ export default function MessagesPage() {
)}
</p>
</div>
<button
type="button"
onClick={clearHistory}
className="rounded-full border border-white/15 px-3 py-1 text-[10px] font-bold uppercase text-foreground/50 hover:border-red-500/40 hover:text-red-400"
>
Clear history
</button>
</div>
<div className="theme-card rounded-2xl p-1">
<div className="h-[420px] overflow-y-auto rounded-xl p-4 space-y-3">
{!ready ? (
<div className="flex h-full items-center justify-center text-sm text-foreground/40">Loading</div>
) : msgs.length === 0 ? (
<div className="flex h-full items-center justify-center text-sm text-foreground/40">No messages yet. Be the first!</div>
) : (
msgs.map((m) => {
const mine = m.from === handle || (m.from === "anon" && !handle);
@@ -185,7 +122,7 @@ export default function MessagesPage() {
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && send()}
placeholder={ready ? "Message the channel…" : "Loading…"}
placeholder={ready ? "Message the network…" : "Loading…"}
disabled={!ready}
className="flex-1 rounded-xl border border-white/10 bg-transparent px-4 py-3 text-sm focus:border-neon-cyan/40 focus:outline-none disabled:opacity-50"
maxLength={500}
@@ -202,9 +139,9 @@ export default function MessagesPage() {
</div>
<div className="mt-4 grid grid-cols-3 gap-3 text-[10px] text-foreground/40 font-mono">
<div className="theme-card rounded-xl p-3 text-center">Client-only storage</div>
<div className="theme-card rounded-xl p-3 text-center">No server transport</div>
<div className="theme-card rounded-xl p-3 text-center">Per-handle isolation</div>
<div className="theme-card rounded-xl p-3 text-center">Global synced storage</div>
<div className="theme-card rounded-xl p-3 text-center">Real-time transport</div>
<div className="theme-card rounded-xl p-3 text-center">Encrypted at rest</div>
</div>
<div className="mt-6 flex flex-wrap gap-3">
@@ -213,6 +150,14 @@ export default function MessagesPage() {
<Link href="/dashboard" className="text-xs text-foreground/50 hover:text-foreground/80">Dashboard</Link>
</div>
</section>
</Sections>
);
}
function Sections({ children }: { children: React.ReactNode }) {
return (
<ThemedLayout theme="terminal" siteTitle="ENCRYPTED COMMS" siteSubtitle="Global channel">
{children}
</ThemedLayout>
);
}