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:
@@ -6,54 +6,13 @@ import { useAccount } from "@/contexts/AccountContext";
|
||||
|
||||
type Msg = {
|
||||
id: string;
|
||||
sender: string;
|
||||
from: string;
|
||||
text: string;
|
||||
time: string;
|
||||
mine: boolean;
|
||||
ts: number;
|
||||
};
|
||||
|
||||
const SEED_MSGS: Omit<Msg, "mine">[] = [
|
||||
{ id: "s1", sender: "void_cartographer", text: "Anyone else notice the latency on the east relay dropped by 40ms this cycle?", time: "01:12" },
|
||||
{ id: "s2", sender: "relay_op", text: "Circuit refresh interval was tuned last night. Should hold for 72h.", time: "01:14" },
|
||||
{ id: "s3", sender: "ledger_moth", text: "New drops posted on /market — entropy dongles and the opsec consult bundle.", time: "01:17" },
|
||||
{ id: "s4", sender: "phantom_q", text: "Verified the hub PGP against the mirror list. All checksums matched.", time: "01:19" },
|
||||
{ id: "s5", sender: "void_cartographer", text: "Anyone have a rec for a good XMR <-> BTC bridge that doesn't log?", time: "01:22" },
|
||||
{ id: "s6", sender: "relay_op", text: "Check /exchange — a few WTB listings went up in the last hour.", time: "01:24" },
|
||||
];
|
||||
|
||||
const AUTO_REPLIES = [
|
||||
"Received. Check your vault for any pending receipts.",
|
||||
"Noted. The channel is ephemeral — nothing persists past this session unless you signed in.",
|
||||
"Copy that. See /forum for threaded discussion on that topic.",
|
||||
"Market is up. Check /drops for the latest scheduled releases.",
|
||||
"Circuit healthy. Reply latency nominal.",
|
||||
"Acknowledged. Keep OPSEC tight and verify mirrors before each session.",
|
||||
];
|
||||
|
||||
const CHAT_KEY = "cyberlux-chat-v1";
|
||||
|
||||
function now() {
|
||||
return new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
||||
}
|
||||
|
||||
function uid() {
|
||||
return Math.random().toString(36).slice(2, 10);
|
||||
}
|
||||
|
||||
function loadHistory(): Msg[] {
|
||||
if (typeof window === "undefined") return [];
|
||||
try {
|
||||
const raw = localStorage.getItem(CHAT_KEY);
|
||||
if (!raw) return [];
|
||||
return JSON.parse(raw) as Msg[];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function saveHistory(msgs: Msg[]) {
|
||||
if (typeof window === "undefined") return;
|
||||
localStorage.setItem(CHAT_KEY, JSON.stringify(msgs.slice(-80)));
|
||||
function now(ts: number) {
|
||||
return new Date(ts).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
||||
}
|
||||
|
||||
export default function ChatWidget() {
|
||||
@@ -65,38 +24,51 @@ export default function ChatWidget() {
|
||||
const [hydrated, setHydrated] = useState(false);
|
||||
const endRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const stored = loadHistory();
|
||||
if (stored.length > 0) {
|
||||
setMessages(stored);
|
||||
} else {
|
||||
const seeded = SEED_MSGS.map((m) => ({ ...m, mine: false }));
|
||||
setMessages(seeded);
|
||||
saveHistory(seeded);
|
||||
const fetchMessages = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch("/api/messages?channel=hub");
|
||||
const data = await res.json();
|
||||
if (data.ok && data.messages) {
|
||||
setMessages(data.messages);
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
setHydrated(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchMessages();
|
||||
setHydrated(true);
|
||||
const interval = setInterval(fetchMessages, 3000);
|
||||
return () => clearInterval(interval);
|
||||
}, [fetchMessages]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hydrated && messages.length > 0) {
|
||||
saveHistory(messages);
|
||||
endRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
}, [messages, hydrated]);
|
||||
|
||||
const send = useCallback(() => {
|
||||
const send = async () => {
|
||||
const text = input.trim();
|
||||
if (!text) return;
|
||||
const mine: Msg = { id: uid(), sender: handle, text, time: now(), mine: true };
|
||||
setMessages((p) => [...p, mine]);
|
||||
setInput("");
|
||||
const delay = 900 + Math.random() * 900;
|
||||
setTimeout(() => {
|
||||
const replyText = AUTO_REPLIES[Math.floor(Math.random() * AUTO_REPLIES.length)]!;
|
||||
const reply: Msg = { id: uid(), sender: "System", text: replyText, time: now(), mine: false };
|
||||
setMessages((p) => [...p, reply]);
|
||||
}, delay);
|
||||
}, [input, handle]);
|
||||
|
||||
// Optimistic update
|
||||
const optimisticMsg: Msg = { id: "temp-" + Date.now(), from: handle, text, ts: Date.now() };
|
||||
setMessages((p) => [...p, optimisticMsg]);
|
||||
|
||||
try {
|
||||
await fetch("/api/messages", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ from: handle, text, channel: "hub" }),
|
||||
});
|
||||
fetchMessages();
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="glass rounded-3xl border border-white/10 p-8">
|
||||
@@ -104,7 +76,7 @@ export default function ChatWidget() {
|
||||
<div>
|
||||
<h2 className="font-orbitron text-3xl font-bold">CHANNEL CHAT</h2>
|
||||
<p className="mt-1 text-sm text-foreground/60">
|
||||
Ephemeral hub channel — stored locally.{" "}
|
||||
Global hub channel — synced live.{" "}
|
||||
{user ? (
|
||||
<span className="text-neon-cyan">Posting as @{user.username}</span>
|
||||
) : (
|
||||
@@ -119,40 +91,47 @@ export default function ChatWidget() {
|
||||
<span className="inline-block h-2 w-2 animate-pulse rounded-full bg-neon-green" />
|
||||
channel active
|
||||
</span>
|
||||
<span>local store</span>
|
||||
<span>live sync</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 h-72 overflow-y-auto rounded-2xl border border-white/10 bg-black/30 p-4 space-y-3">
|
||||
{messages.map((msg) => (
|
||||
<div key={msg.id} className={`flex flex-col ${msg.mine ? "items-end" : "items-start"}`}>
|
||||
<div className="mb-0.5 flex items-center gap-2">
|
||||
<span
|
||||
className={`text-xs font-medium ${
|
||||
msg.mine
|
||||
? "text-neon-cyan"
|
||||
: msg.sender === "System"
|
||||
? "text-neon-purple"
|
||||
: "text-foreground/70"
|
||||
}`}
|
||||
>
|
||||
{msg.mine ? `@${handle}` : msg.sender === "System" ? "⚡ System" : `@${msg.sender}`}
|
||||
</span>
|
||||
<span className="text-[10px] text-foreground/30">{msg.time}</span>
|
||||
</div>
|
||||
<div
|
||||
className={`max-w-[80%] rounded-2xl px-4 py-2 text-sm ${
|
||||
msg.mine
|
||||
? "bg-neon-cyan/15 text-neon-cyan"
|
||||
: msg.sender === "System"
|
||||
? "bg-neon-purple/10 text-neon-purple/90"
|
||||
: "bg-white/5 text-foreground/90"
|
||||
}`}
|
||||
>
|
||||
{msg.text}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{messages.length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center text-sm text-foreground/40">No messages yet. Be the first!</div>
|
||||
) : (
|
||||
messages.map((msg) => {
|
||||
const mine = msg.from === handle || (msg.from === "anon" && !user);
|
||||
return (
|
||||
<div key={msg.id} className={`flex flex-col ${mine ? "items-end" : "items-start"}`}>
|
||||
<div className="mb-0.5 flex items-center gap-2">
|
||||
<span
|
||||
className={`text-xs font-medium ${
|
||||
mine
|
||||
? "text-neon-cyan"
|
||||
: msg.from === "System"
|
||||
? "text-neon-purple"
|
||||
: "text-foreground/70"
|
||||
}`}
|
||||
>
|
||||
{mine ? `@${handle}` : msg.from === "System" ? "⚡ System" : `@${msg.from}`}
|
||||
</span>
|
||||
<span className="text-[10px] text-foreground/30">{now(msg.ts)}</span>
|
||||
</div>
|
||||
<div
|
||||
className={`max-w-[80%] rounded-2xl px-4 py-2 text-sm ${
|
||||
mine
|
||||
? "bg-neon-cyan/15 text-neon-cyan"
|
||||
: msg.from === "System"
|
||||
? "bg-neon-purple/10 text-neon-purple/90"
|
||||
: "bg-white/5 text-foreground/90"
|
||||
}`}
|
||||
>
|
||||
{msg.text}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
<div ref={endRef} />
|
||||
</div>
|
||||
|
||||
@@ -178,7 +157,7 @@ export default function ChatWidget() {
|
||||
|
||||
<div className="mt-6 grid grid-cols-2 gap-4 md:grid-cols-4">
|
||||
{[
|
||||
{ icon: "🔒", label: "Client storage", sub: "Never leaves your browser" },
|
||||
{ icon: "🌍", label: "Global Sync", sub: "Talk to everyone" },
|
||||
{ icon: "💬", label: "Open channel", sub: "Hub-wide thread" },
|
||||
{ icon: "📬", label: "Forum threads", sub: "/forum for persistence" },
|
||||
{ icon: "📦", label: "Market drops", sub: "/drops for schedule" },
|
||||
|
||||
Reference in New Issue
Block a user