10x every page: real interactions, kill fake content, wire everything
- ChatWidget: remove illegal seeds, real localStorage per-handle chat, honest bot replies about market/forum/funds - ForumBoard: wire to real forumState (loadForum/addThread/vote), kill fake stats and illegal seed posts - Home page: privacy features list reflects reality, footer links real - Links: kill all alert() calls, replace fake onions with real clearnet privacy resources + internal route grid - Support: per-coin copied state, env-driven addresses, real BTC addr - Inner circle: wire to AccountContext, tier system from LUX balance, remove hardcoded admin/shadow credentials and fake trading signals - Drop box: real sealed-note localStorage system, honest about no anonymous upload capability, real file picker with receipt - Messages: fully functional per-handle localStorage chat, AI-style contextual bot replies, clear history, honest about local storage - Wallets: pivot from fake PayPal accounts to Digital Access Passes, wire Buy Now to cart via ShopProduct interface - Testimonials: wire submit form to localStorage, interactive star rating 1-10, display submitted reviews above the fold - Raffle: use real merchant BTC address, real per-handle entry storage, honest LUX-only prize disclaimer, fix 0x address - Drops/Lotto: real number picker 1-49 with Quick Pick, ticket submission, match display against drawn numbers, demo disclaimer - Sanctuary: real 4-4-6-2 breathing timer, meditation passage with timer, candle-lighting with localStorage notes - Game: full playable Void Pong with canvas physics, CPU AI, scoring, rally counter, localStorage high score - Security analysis: honest architecture breakdown with real grades, layer-by-layer analysis, practical OPSEC guide, fiction banner - Trust: compute real scores from actual localStorage data (LUX, USD, forum posts, testimonials), FAQ accordion Made-with: Cursor
This commit is contained in:
@@ -1,173 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
|
||||
/** Hub CTA — real provisioning lives on /sign-up (local vault + per-handle balance). */
|
||||
const AccountCreation = () => {
|
||||
const [step, setStep] = useState(1);
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [recoveryPhrase, setRecoveryPhrase] = useState("");
|
||||
const [encryptionKey, setEncryptionKey] = useState("");
|
||||
const [generated, setGenerated] = useState(false);
|
||||
|
||||
const generateRecovery = () => {
|
||||
const phrases = [
|
||||
"phantom quantum glacier nexus",
|
||||
"crimson velvet paradox silence",
|
||||
"neon abyss whisper eclipse",
|
||||
"zero trace ghost protocol",
|
||||
];
|
||||
const randomPhrase = phrases[Math.floor(Math.random() * phrases.length)];
|
||||
setRecoveryPhrase(randomPhrase);
|
||||
setEncryptionKey(btoa(Date.now().toString()).slice(0, 16));
|
||||
setGenerated(true);
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (step < 3) {
|
||||
setStep(step + 1);
|
||||
} else {
|
||||
alert("Identity provisioned. Keys derived locally — backup your recovery phrase offline.");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="glass mx-auto max-w-2xl rounded-3xl border border-white/10 p-10">
|
||||
<h2 className="mb-8 font-orbitron text-4xl font-bold text-center">CREATE SHADOW IDENTITY</h2>
|
||||
|
||||
{/* Steps */}
|
||||
<div className="mb-10 flex justify-between">
|
||||
{[1, 2, 3].map((s) => (
|
||||
<div key={s} className="flex flex-col items-center">
|
||||
<div
|
||||
className={`flex h-12 w-12 items-center justify-center rounded-full border-2 text-lg font-bold ${step >= s
|
||||
? "border-neon-cyan bg-neon-cyan/20 text-neon-cyan"
|
||||
: "border-white/20 text-foreground/40"
|
||||
}`}
|
||||
>
|
||||
{s}
|
||||
</div>
|
||||
<div className="mt-2 text-sm">
|
||||
{s === 1 && "Credentials"}
|
||||
{s === 2 && "Recovery"}
|
||||
{s === 3 && "Encryption"}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
{step === 1 && (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label className="mb-2 block font-medium">Shadow Alias</label>
|
||||
<input
|
||||
type="text"
|
||||
className="glass w-full rounded-xl border border-white/10 bg-transparent p-4"
|
||||
placeholder="e.g., Ghost_23"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<p className="mt-2 text-sm text-foreground/60">Never your real name. This alias is encrypted.</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-2 block font-medium">Zero‑Knowledge Password</label>
|
||||
<input
|
||||
type="password"
|
||||
className="glass w-full rounded-xl border border-white/10 bg-transparent p-4"
|
||||
placeholder="••••••••••"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<p className="mt-2 text-sm text-foreground/60">We never store your password. It’s used to derive your encryption key.</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 2 && (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label className="mb-2 block font-medium">Recovery Phrase</label>
|
||||
<div className="glass rounded-xl border border-neon-cyan/30 p-6 font-mono text-center text-lg">
|
||||
{generated ? recoveryPhrase : "Click generate to create"}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="mt-4 w-full rounded-full bg-gradient-to-r from-neon-cyan to-neon-purple py-3 font-bold text-background"
|
||||
onClick={generateRecovery}
|
||||
>
|
||||
GENERATE RECOVERY PHRASE
|
||||
</button>
|
||||
<p className="mt-4 text-sm text-foreground/60">
|
||||
Write this down physically. It’s the only way to recover your account. We do not store it.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 3 && (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label className="mb-2 block font-medium">Encryption Key</label>
|
||||
<div className="glass rounded-xl border border-neon-green/30 p-6 font-mono text-center text-lg">
|
||||
{encryptionKey || "Not generated"}
|
||||
</div>
|
||||
<p className="mt-4 text-sm text-foreground/60">
|
||||
This key encrypts all your data locally. It is derived from your password and never leaves your device.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl bg-gradient-to-r from-neon-cyan/10 to-neon-purple/10 p-6">
|
||||
<h3 className="font-bold">Identity Summary</h3>
|
||||
<div className="mt-4 grid grid-cols-2 gap-4 text-sm">
|
||||
<div>Alias</div>
|
||||
<div className="font-mono">{username || "—"}</div>
|
||||
<div>Recovery Phrase</div>
|
||||
<div className="font-mono truncate">{recoveryPhrase || "—"}</div>
|
||||
<div>Encryption</div>
|
||||
<div className="font-mono text-neon-green">AES‑256‑GCM</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-10 flex justify-between">
|
||||
<button
|
||||
type="button"
|
||||
className="glass rounded-full px-8 py-3 font-medium disabled:opacity-30"
|
||||
onClick={() => setStep(step - 1)}
|
||||
disabled={step === 1}
|
||||
>
|
||||
← Back
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="rounded-full bg-gradient-to-r from-neon-cyan to-neon-purple px-10 py-3 font-bold text-background"
|
||||
>
|
||||
{step === 3 ? "COMPLETE IDENTITY CREATION" : "CONTINUE →"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="mt-10 border-t border-white/10 pt-8 text-center text-sm text-foreground/40">
|
||||
<p>Keys and aliases never leave this browser profile. Clear storage = identity loss.</p>
|
||||
<p className="mt-4 text-foreground/55">
|
||||
For a <strong className="text-neon-cyan/90">live stall session</strong> tied to wallet and activity feeds, use{" "}
|
||||
<Link href="/sign-up" className="text-neon-cyan underline hover:text-neon-purple">
|
||||
create account
|
||||
</Link>{" "}
|
||||
or{" "}
|
||||
<Link href="/sign-in" className="text-neon-cyan underline hover:text-neon-purple">
|
||||
sign in
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
<h2 className="mb-4 text-center font-orbitron text-3xl font-bold md:text-4xl">Create an account</h2>
|
||||
<p className="text-center text-foreground/70">
|
||||
One handle for forum, exchange, barter, checkout, vault, and Bitcoin-funded USD balance on this origin.
|
||||
Credentials stay in your browser; use{" "}
|
||||
<Link href="/account/hidden-services" className="text-neon-cyan underline">
|
||||
Hidden services
|
||||
</Link>{" "}
|
||||
to copy your identity to another .onion host.
|
||||
</p>
|
||||
<div className="mt-10 flex flex-col items-center justify-center gap-4 sm:flex-row">
|
||||
<Link
|
||||
href="/sign-up"
|
||||
className="inline-block rounded-full bg-gradient-to-r from-neon-cyan to-neon-purple px-10 py-4 text-center font-bold text-background"
|
||||
>
|
||||
Register
|
||||
</Link>
|
||||
<Link
|
||||
href="/sign-in"
|
||||
className="inline-block rounded-full border border-white/20 px-10 py-4 text-center font-medium hover:border-neon-cyan/50"
|
||||
>
|
||||
Sign in
|
||||
</Link>
|
||||
</div>
|
||||
<p className="mt-8 text-center text-sm text-foreground/50">
|
||||
Add funds after sign-in:{" "}
|
||||
<Link href="/account/add-funds" className="text-neon-green underline">
|
||||
/account/add-funds
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountCreation;
|
||||
export default AccountCreation;
|
||||
|
||||
@@ -5,13 +5,13 @@ import { AccountProvider } from "@/contexts/AccountContext";
|
||||
import { CartProvider } from "@/contexts/CartContext";
|
||||
import { WalletProvider } from "@/contexts/WalletContext";
|
||||
|
||||
/** Client-only wrappers so wallet + persistent account work on every route. */
|
||||
/** Account must wrap wallet so balance + vault ledger are keyed by the signed-in handle. */
|
||||
export default function AppProviders({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<WalletProvider>
|
||||
<AccountProvider>
|
||||
<AccountProvider>
|
||||
<WalletProvider>
|
||||
<CartProvider>{children}</CartProvider>
|
||||
</AccountProvider>
|
||||
</WalletProvider>
|
||||
</WalletProvider>
|
||||
</AccountProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,156 +1,195 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
import Link from "next/link";
|
||||
import { useAccount } from "@/contexts/AccountContext";
|
||||
|
||||
const ChatWidget = () => {
|
||||
const [messages, setMessages] = useState([
|
||||
{ id: 1, sender: "System", text: "Welcome to the encrypted channel. All messages are end‑to‑end encrypted.", time: "12:00" },
|
||||
{ id: 2, sender: "Ghost", text: "Has anyone tested the new stimulant batch?", time: "12:05" },
|
||||
{ id: 3, sender: "Vendor_X", text: "Batch #8 passes all purity tests. Available for bulk.", time: "12:07" },
|
||||
{ id: 4, sender: "Anonymous", text: "Need a EU passport within 48 hours. DM if you can deliver.", time: "12:10" },
|
||||
]);
|
||||
type Msg = {
|
||||
id: string;
|
||||
sender: string;
|
||||
text: string;
|
||||
time: string;
|
||||
mine: boolean;
|
||||
};
|
||||
|
||||
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)));
|
||||
}
|
||||
|
||||
export default function ChatWidget() {
|
||||
const { user } = useAccount();
|
||||
const handle = user?.displayName ?? user?.username ?? "anon";
|
||||
|
||||
const [messages, setMessages] = useState<Msg[]>([]);
|
||||
const [input, setInput] = useState("");
|
||||
/** Set after mount so SSR HTML matches first client paint (no Math.random in initial state). */
|
||||
const [encryptionKey, setEncryptionKey] = useState("");
|
||||
const chatContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [hydrated, setHydrated] = useState(false);
|
||||
const endRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setEncryptionKey("session_" + Math.random().toString(36).substring(2, 9));
|
||||
const stored = loadHistory();
|
||||
if (stored.length > 0) {
|
||||
setMessages(stored);
|
||||
} else {
|
||||
const seeded = SEED_MSGS.map((m) => ({ ...m, mine: false }));
|
||||
setMessages(seeded);
|
||||
saveHistory(seeded);
|
||||
}
|
||||
setHydrated(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (chatContainerRef.current) {
|
||||
chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight;
|
||||
if (hydrated && messages.length > 0) {
|
||||
saveHistory(messages);
|
||||
endRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
}, [messages]);
|
||||
}, [messages, hydrated]);
|
||||
|
||||
const handleSend = () => {
|
||||
if (!input.trim()) return;
|
||||
const newMessage = {
|
||||
id: messages.length + 1,
|
||||
sender: "You",
|
||||
text: input,
|
||||
time: new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }),
|
||||
};
|
||||
setMessages([...messages, newMessage]);
|
||||
const send = useCallback(() => {
|
||||
const text = input.trim();
|
||||
if (!text) return;
|
||||
const mine: Msg = { id: uid(), sender: handle, text, time: now(), mine: true };
|
||||
setMessages((p) => [...p, mine]);
|
||||
setInput("");
|
||||
|
||||
// Simulate a reply after a delay
|
||||
const delay = 900 + Math.random() * 900;
|
||||
setTimeout(() => {
|
||||
const replies = [
|
||||
"Received. Encryption verified.",
|
||||
"I can help with that. Check your vault.",
|
||||
"New drop incoming in 24 hours.",
|
||||
"Your request has been logged.",
|
||||
];
|
||||
const randomReply = replies[Math.floor(Math.random() * replies.length)];
|
||||
const replyMessage = {
|
||||
id: messages.length + 2,
|
||||
sender: "System",
|
||||
text: randomReply,
|
||||
time: new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }),
|
||||
};
|
||||
setMessages(prev => [...prev, replyMessage]);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const handleEncrypt = () => {
|
||||
alert(`Session key: ${encryptionKey}\nAll messages are encrypted with this key.`);
|
||||
};
|
||||
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]);
|
||||
|
||||
return (
|
||||
<div className="glass rounded-3xl border border-white/10 p-8">
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div className="mb-6 flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="font-orbitron text-3xl font-bold">ENCRYPTED CHAT</h2>
|
||||
<p className="text-foreground/60">End‑to‑end encrypted messaging. No logs, no traces.</p>
|
||||
<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.{" "}
|
||||
{user ? (
|
||||
<span className="text-neon-cyan">Posting as @{user.username}</span>
|
||||
) : (
|
||||
<Link href="/sign-in" className="text-neon-cyan underline">
|
||||
Sign in to tag your handle
|
||||
</Link>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-3 text-[10px] font-mono uppercase tracking-wider text-foreground/40">
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="inline-block h-2 w-2 animate-pulse rounded-full bg-neon-green" />
|
||||
channel active
|
||||
</span>
|
||||
<span>local store</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleEncrypt}
|
||||
className="rounded-full bg-gradient-to-r from-neon-cyan to-neon-purple px-6 py-3 font-bold text-background"
|
||||
>
|
||||
🔐 SHOW SESSION KEY
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Chat messages */}
|
||||
<div
|
||||
ref={chatContainerRef}
|
||||
className="glass mb-6 h-96 overflow-y-auto rounded-2xl border border-white/10 p-6"
|
||||
>
|
||||
<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={`mb-4 ${msg.sender === "You" ? "text-right" : ""}`}
|
||||
>
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
<span className={`inline-block rounded-full px-3 py-1 text-xs font-medium ${msg.sender === "You"
|
||||
? "bg-neon-cyan/20 text-neon-cyan"
|
||||
: msg.sender === "System"
|
||||
? "bg-neon-purple/20 text-neon-purple"
|
||||
: "bg-white/10"
|
||||
}`}>
|
||||
{msg.sender}
|
||||
<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-xs text-foreground/40">{msg.time}</span>
|
||||
<span className="text-[10px] text-foreground/30">{msg.time}</span>
|
||||
</div>
|
||||
<div className={`glass inline-block max-w-[80%] rounded-2xl px-4 py-3 ${msg.sender === "You"
|
||||
? "bg-gradient-to-r from-neon-cyan/20 to-neon-cyan/10"
|
||||
: "bg-white/5"
|
||||
}`}>
|
||||
<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>
|
||||
))}
|
||||
<div ref={endRef} />
|
||||
</div>
|
||||
|
||||
{/* Input area */}
|
||||
<div className="flex gap-4">
|
||||
<div className="flex gap-3">
|
||||
<input
|
||||
type="text"
|
||||
className="glass flex-1 rounded-2xl border border-white/10 bg-transparent px-6 py-4"
|
||||
placeholder="Type your encrypted message..."
|
||||
className="flex-1 rounded-2xl border border-white/10 bg-black/40 px-5 py-3 text-sm focus:border-neon-cyan/40 focus:outline-none"
|
||||
placeholder={user ? `Message as @${user.username}…` : "Message the channel…"}
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleSend()}
|
||||
onKeyDown={(e) => e.key === "Enter" && send()}
|
||||
maxLength={500}
|
||||
/>
|
||||
<button
|
||||
onClick={handleSend}
|
||||
className="rounded-full bg-gradient-to-r from-neon-cyan to-neon-purple px-8 py-4 font-bold text-background"
|
||||
type="button"
|
||||
onClick={send}
|
||||
disabled={!input.trim()}
|
||||
className="rounded-2xl bg-gradient-to-r from-neon-cyan to-neon-purple px-6 py-3 text-sm font-bold text-background disabled:opacity-40"
|
||||
>
|
||||
SEND
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Chat features */}
|
||||
<div className="mt-8 grid grid-cols-2 gap-6 md:grid-cols-4">
|
||||
<div className="glass rounded-2xl border border-white/10 p-4 text-center">
|
||||
<div className="text-2xl">🔒</div>
|
||||
<div className="mt-2 text-sm font-bold">AES‑256‑GCM</div>
|
||||
<div className="text-xs text-foreground/60">Encryption</div>
|
||||
</div>
|
||||
<div className="glass rounded-2xl border border-white/10 p-4 text-center">
|
||||
<div className="text-2xl">⚡</div>
|
||||
<div className="mt-2 text-sm font-bold">Zero‑Knowledge</div>
|
||||
<div className="text-xs text-foreground/60">No server storage</div>
|
||||
</div>
|
||||
<div className="glass rounded-2xl border border-white/10 p-4 text-center">
|
||||
<div className="text-2xl">🌐</div>
|
||||
<div className="mt-2 text-sm font-bold">WebRTC</div>
|
||||
<div className="text-xs text-foreground/60">P2P possible</div>
|
||||
</div>
|
||||
<div className="glass rounded-2xl border border-white/10 p-4 text-center">
|
||||
<div className="text-2xl">🕶️</div>
|
||||
<div className="mt-2 text-sm font-bold">Self‑Destruct</div>
|
||||
<div className="text-xs text-foreground/60">Messages expire</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 text-center text-xs text-foreground/40">
|
||||
<p>Ephemeral client-side queue — nothing leaves this browser profile until a real transport is configured.</p>
|
||||
<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: "Open channel", sub: "Hub-wide thread" },
|
||||
{ icon: "📬", label: "Forum threads", sub: "/forum for persistence" },
|
||||
{ icon: "📦", label: "Market drops", sub: "/drops for schedule" },
|
||||
].map((f) => (
|
||||
<div key={f.label} className="glass rounded-2xl border border-white/10 p-4 text-center">
|
||||
<div className="text-2xl">{f.icon}</div>
|
||||
<div className="mt-2 text-sm font-bold">{f.label}</div>
|
||||
<div className="text-xs text-foreground/50">{f.sub}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatWidget;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import Link from "next/link";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useAccount } from "@/contexts/AccountContext";
|
||||
import { useCart } from "@/contexts/CartContext";
|
||||
import { useWallet } from "@/contexts/WalletContext";
|
||||
import { getMerchantBtcAddress, isMerchantBtcConfigured } from "@/lib/merchantBtc";
|
||||
@@ -17,7 +18,6 @@ const CheckoutFlow = () => {
|
||||
const { lines, setLineQty, removeLine, clearCart, hydrated: cartHydrated } = useCart();
|
||||
const [step, setStep] = useState(1);
|
||||
const [paymentMethod, setPaymentMethod] = useState<"crypto" | "guest" | "card">("crypto");
|
||||
const [cryptoType, setCryptoType] = useState<"BTC" | "ETH" | "XMR">("BTC");
|
||||
const [guestEmail, setGuestEmail] = useState("");
|
||||
const [encryptionLevel, setEncryptionLevel] = useState<"standard" | "enhanced" | "quantum">("enhanced");
|
||||
const [btcUsd, setBtcUsd] = useState<number | null>(null);
|
||||
@@ -26,8 +26,10 @@ const CheckoutFlow = () => {
|
||||
const [verifyBusy, setVerifyBusy] = useState(false);
|
||||
const [payError, setPayError] = useState<string | null>(null);
|
||||
const [orderComplete, setOrderComplete] = useState(false);
|
||||
const [completedOrderId, setCompletedOrderId] = useState<string | null>(null);
|
||||
|
||||
const { usdStoreCredit, spendUsdStoreCredit, verifyBtcDeposit } = useWallet();
|
||||
const { user } = useAccount();
|
||||
const { usdStoreCredit, spendUsdStoreCredit, verifyBtcDeposit, earnLuxCredits, addVaultReceipt } = useWallet();
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
@@ -107,14 +109,34 @@ const CheckoutFlow = () => {
|
||||
const tryCompleteOrder = () => {
|
||||
setPayError(null);
|
||||
if (paymentMethod === "crypto") {
|
||||
if (!user) {
|
||||
setPayError("Sign in to charge your verified Bitcoin-funded USD balance.");
|
||||
return;
|
||||
}
|
||||
if (!spendUsdStoreCredit(orderTotalUsd)) {
|
||||
setPayError(
|
||||
`Insufficient USD balance. You need $${orderTotalUsd.toFixed(2)}. Send Bitcoin to the address below, wait for at least one confirmation, then paste the transaction ID and click Verify.`,
|
||||
`Insufficient USD balance. You need $${orderTotalUsd.toFixed(2)}. Add funds under Account → Add funds, then verify your txid.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const oid =
|
||||
typeof crypto !== "undefined" && "randomUUID" in crypto
|
||||
? `CYBR-${crypto.randomUUID().slice(0, 8).toUpperCase()}`
|
||||
: `CYBR-${Date.now().toString(36).toUpperCase()}`;
|
||||
setCompletedOrderId(oid);
|
||||
setOrderComplete(true);
|
||||
if (user && paymentMethod === "crypto") {
|
||||
const lux = Math.min(500, Math.max(25, Math.floor(orderTotalUsd * 2)));
|
||||
earnLuxCredits(lux);
|
||||
addVaultReceipt({
|
||||
id: oid,
|
||||
title: `Order ${oid}`,
|
||||
desc: `Checkout ${orderTotalUsd.toFixed(2)} USD · ${encryptionLevel} tier.`,
|
||||
date: new Date().toISOString().slice(0, 10),
|
||||
severity: "normal",
|
||||
});
|
||||
}
|
||||
clearCart();
|
||||
};
|
||||
|
||||
@@ -251,9 +273,17 @@ const CheckoutFlow = () => {
|
||||
<span className="font-orbitron">${usdStoreCredit.toFixed(2)}</span>
|
||||
<span className="text-foreground/60">
|
||||
{" "}
|
||||
— use after Bitcoin deposit is verified (on-chain).
|
||||
— per signed-in handle after verified Bitcoin deposits.
|
||||
</span>
|
||||
</div>
|
||||
{paymentMethod === "crypto" && !user ? (
|
||||
<p className="mb-4 rounded-lg border border-amber-500/30 bg-amber-500/10 px-4 py-3 text-sm text-amber-200">
|
||||
<Link href="/sign-in?next=/checkout" className="font-bold underline">
|
||||
Sign in
|
||||
</Link>{" "}
|
||||
to spend USD balance. You can still browse deposit instructions below.
|
||||
</p>
|
||||
) : null}
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
<button
|
||||
type="button"
|
||||
@@ -273,7 +303,7 @@ const CheckoutFlow = () => {
|
||||
>
|
||||
<div className="mb-4 text-4xl">👤</div>
|
||||
<div className="font-bold">Guest Checkout</div>
|
||||
<div className="mt-2 text-sm text-foreground/60">Demo — no USD charge</div>
|
||||
<div className="mt-2 text-sm text-foreground/60">No USD charge — order completes locally only</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -282,52 +312,29 @@ const CheckoutFlow = () => {
|
||||
onClick={() => setPaymentMethod("card")}
|
||||
>
|
||||
<div className="mb-4 text-4xl">💳</div>
|
||||
<div className="font-bold">Card (Secure)</div>
|
||||
<div className="mt-2 text-sm text-foreground/60">Demo — no USD charge</div>
|
||||
<div className="font-bold">Card</div>
|
||||
<div className="mt-2 text-sm text-foreground/60">Not enabled — use Bitcoin balance instead</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{paymentMethod === "crypto" && (
|
||||
<div className="mt-8">
|
||||
<label className="mb-2 block font-medium">Select Cryptocurrency</label>
|
||||
<div className="flex gap-4">
|
||||
{(["BTC", "ETH", "XMR"] as const).map((coin) => (
|
||||
<button
|
||||
key={coin}
|
||||
type="button"
|
||||
className={`glass flex-1 rounded-xl border py-4 ${cryptoType === coin ? "border-neon-cyan bg-neon-cyan/10" : "border-white/10"
|
||||
}`}
|
||||
onClick={() => setCryptoType(coin)}
|
||||
>
|
||||
<div className="text-2xl">{coin === "BTC" ? "₿" : coin === "ETH" ? "Ξ" : "⏣"}</div>
|
||||
<div className="font-bold">{coin}</div>
|
||||
</button>
|
||||
))}
|
||||
<div className="mt-8 space-y-4 rounded-xl bg-white/5 p-4">
|
||||
<div className="text-sm text-foreground/60">Bitcoin-only · estimated order total after encryption step</div>
|
||||
<div className="font-orbitron text-2xl text-neon-cyan">
|
||||
${orderTotalUsd.toLocaleString()} USD
|
||||
{orderTotalBtc != null && btcUsd != null && (
|
||||
<span className="ml-3 text-lg text-foreground/70">
|
||||
(≈ {orderTotalBtc} BTC @ ${btcUsd.toLocaleString()}/BTC)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{cryptoType !== "BTC" && (
|
||||
<p className="mt-4 rounded-lg bg-white/5 p-4 text-sm text-foreground/70">
|
||||
Deposits that add USD balance use <strong>Bitcoin</strong> only. Switch to BTC to see the
|
||||
deposit address. (ETH/XMR shown for display.)
|
||||
</p>
|
||||
)}
|
||||
{cryptoType === "BTC" && (
|
||||
<div className="mt-6 space-y-4 rounded-xl bg-white/5 p-4">
|
||||
<div className="text-sm text-foreground/60">Estimated order total after encryption step</div>
|
||||
<div className="font-orbitron text-2xl text-neon-cyan">
|
||||
${orderTotalUsd.toLocaleString()} USD
|
||||
{orderTotalBtc != null && btcUsd != null && (
|
||||
<span className="ml-3 text-lg text-foreground/70">
|
||||
(≈ {orderTotalBtc} BTC @ ${btcUsd.toLocaleString()}/BTC)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-foreground/50">
|
||||
Final total includes the encryption add-on you pick in the next step. Fund your balance
|
||||
with <strong>any amount</strong> of BTC; verified value is credited in USD at spot rate.
|
||||
You can pay this order once your balance covers the total.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<p className="text-xs text-foreground/50">
|
||||
Final total includes the encryption add-on you pick in the next step. Add funds on{" "}
|
||||
<Link href="/account/add-funds" className="text-neon-cyan underline">
|
||||
/account/add-funds
|
||||
</Link>{" "}
|
||||
(same balance here and at checkout).
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -342,13 +349,13 @@ const CheckoutFlow = () => {
|
||||
onChange={(e) => setGuestEmail(e.target.value)}
|
||||
/>
|
||||
<p className="mt-2 text-sm text-foreground/60">
|
||||
Guest checkout is a demo and does not charge your USD balance.
|
||||
Guest checkout does not touch your USD balance; nothing is sent to a payment processor.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-10 border-t border-white/10 pt-8">
|
||||
<h4 className="mb-4 font-orbitron text-lg font-bold text-neon-cyan">Bitcoin deposit (all checkouts)</h4>
|
||||
<h4 className="mb-4 font-orbitron text-lg font-bold text-neon-cyan">Bitcoin deposit · processor</h4>
|
||||
{!merchantReady ? (
|
||||
<p className="text-sm text-amber-300/90">
|
||||
Set <code className="rounded bg-white/10 px-1">NEXT_PUBLIC_MERCHANT_BTC_ADDRESS</code> and{" "}
|
||||
@@ -358,8 +365,12 @@ const CheckoutFlow = () => {
|
||||
) : (
|
||||
<>
|
||||
<p className="mb-3 text-sm text-foreground/70">
|
||||
Send Bitcoin to this address. After the network confirms your payment (at least one
|
||||
block), paste the transaction ID to add the equivalent USD to your balance.
|
||||
Send Bitcoin to this address. After at least one confirmation, paste the txid — credits apply
|
||||
to the signed-in handle only (see{" "}
|
||||
<Link href="/account/add-funds" className="text-neon-cyan underline">
|
||||
Add funds
|
||||
</Link>
|
||||
).
|
||||
</p>
|
||||
<div className="rounded-xl border border-white/10 bg-black/40 p-4 font-mono text-sm break-all">
|
||||
{merchantAddr}
|
||||
@@ -471,7 +482,7 @@ const CheckoutFlow = () => {
|
||||
<div className="mt-2 flex justify-between text-sm">
|
||||
<span className="text-foreground/60">Payment path</span>
|
||||
<span className="font-bold">
|
||||
{paymentMethod === "crypto" ? "Crypto (USD balance)" : "Demo (guest/card)"}
|
||||
{paymentMethod === "crypto" ? "Bitcoin-funded USD balance" : "Guest / card (local only)"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -481,7 +492,7 @@ const CheckoutFlow = () => {
|
||||
onClick={tryCompleteOrder}
|
||||
className="rounded-full bg-gradient-to-r from-neon-cyan to-neon-purple px-10 py-3 font-bold text-background"
|
||||
>
|
||||
{paymentMethod === "crypto" ? "Charge my USD balance & complete" : "Complete demo order"}
|
||||
{paymentMethod === "crypto" ? "Charge my USD balance & complete" : "Complete order (local)"}
|
||||
</button>
|
||||
{paymentMethod === "crypto" && (
|
||||
<p className="mx-auto mt-6 max-w-xl text-xs text-foreground/50">
|
||||
@@ -506,7 +517,9 @@ const CheckoutFlow = () => {
|
||||
</p>
|
||||
<div className="my-10">
|
||||
<div className="glass mx-auto max-w-md rounded-2xl border border-white/10 p-6">
|
||||
<div className="font-orbitron text-2xl font-bold text-neon-cyan">ORDER #CYBR‑9A2F‑B8E1</div>
|
||||
<div className="font-orbitron text-2xl font-bold text-neon-cyan">
|
||||
ORDER #{completedOrderId ?? "—"}
|
||||
</div>
|
||||
<div className="mt-4 grid grid-cols-2 gap-4 text-sm">
|
||||
<div className="text-left text-foreground/60">Amount</div>
|
||||
<div className="text-right font-bold">${orderTotalUsd.toLocaleString()} USD</div>
|
||||
@@ -514,7 +527,7 @@ const CheckoutFlow = () => {
|
||||
<div className="text-right font-bold">{encryptionLevel.toUpperCase()}</div>
|
||||
<div className="text-left text-foreground/60">Payment</div>
|
||||
<div className="truncate text-right font-mono text-neon-green">
|
||||
{paymentMethod === "crypto" ? "USD balance (BTC-funded)" : "Demo guest/card"}
|
||||
{paymentMethod === "crypto" ? "USD balance (BTC-funded)" : "Guest or card (no processor)"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,220 +1,168 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useAccount } from "@/contexts/AccountContext";
|
||||
import {
|
||||
loadForum,
|
||||
addThread,
|
||||
displayScore,
|
||||
setVoteForThread,
|
||||
getVoteForThread,
|
||||
type ForumThread,
|
||||
} from "@/lib/forumState";
|
||||
|
||||
const ForumBoard = () => {
|
||||
const [posts, setPosts] = useState([
|
||||
{
|
||||
id: 1,
|
||||
user: "GhostInTheShell",
|
||||
avatar: "👻",
|
||||
timestamp: "2 hours ago",
|
||||
content: "Has anyone tested the new neural stimulant? Need reports on side‑effects.",
|
||||
upvotes: 42,
|
||||
replies: 12,
|
||||
category: "Bio‑Enhancements",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
user: "ZeroCool",
|
||||
avatar: "🕵️",
|
||||
timestamp: "5 hours ago",
|
||||
content: "Quantum counterfeit notes batch #8 passes all validation tests. Available for bulk orders.",
|
||||
upvotes: 89,
|
||||
replies: 24,
|
||||
category: "Financial",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
user: "CypherPunk",
|
||||
avatar: "🔐",
|
||||
timestamp: "1 day ago",
|
||||
content: "New darknet router vulnerability discovered. Patch your firmware immediately.",
|
||||
upvotes: 156,
|
||||
replies: 47,
|
||||
category: "Security",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
user: "Ethereal",
|
||||
avatar: "🌌",
|
||||
timestamp: "2 days ago",
|
||||
content: "Looking for reliable identity pack vendor with EU passports. DM with reputation score.",
|
||||
upvotes: 33,
|
||||
replies: 8,
|
||||
category: "Identity",
|
||||
},
|
||||
]);
|
||||
const CATEGORIES = ["All", "Security", "Market", "OPSEC", "Tech", "General"];
|
||||
|
||||
export default function ForumBoard() {
|
||||
const { user } = useAccount();
|
||||
const [threads, setThreads] = useState<ForumThread[]>([]);
|
||||
const [newPost, setNewPost] = useState("");
|
||||
const [selectedCategory, setSelectedCategory] = useState("All");
|
||||
const [selectedCat, setSelectedCat] = useState("All");
|
||||
const [voteBump, setVoteBump] = useState(0);
|
||||
const [posting, setPosting] = useState(false);
|
||||
|
||||
const categories = ["All", "Bio‑Enhancements", "Financial", "Security", "Identity", "Weapons", "Chemicals"];
|
||||
useEffect(() => {
|
||||
setThreads(loadForum().slice(0, 8));
|
||||
}, []);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!newPost.trim()) return;
|
||||
const newPostObj = {
|
||||
id: posts.length + 1,
|
||||
user: "Anonymous",
|
||||
avatar: "🕶️",
|
||||
timestamp: "Just now",
|
||||
content: newPost,
|
||||
upvotes: 0,
|
||||
replies: 0,
|
||||
category: "Uncategorized",
|
||||
};
|
||||
setPosts([newPostObj, ...posts]);
|
||||
if (!newPost.trim() || posting) return;
|
||||
setPosting(true);
|
||||
const thread = addThread({
|
||||
title: newPost.slice(0, 80),
|
||||
body: newPost,
|
||||
author: user?.username ?? "anon",
|
||||
topicSlug: "general",
|
||||
category: "General",
|
||||
replies: [],
|
||||
});
|
||||
setThreads([thread, ...threads].slice(0, 8));
|
||||
setNewPost("");
|
||||
setPosting(false);
|
||||
};
|
||||
|
||||
const filteredPosts = selectedCategory === "All"
|
||||
? posts
|
||||
: posts.filter(p => p.category === selectedCategory);
|
||||
const handleVote = (id: string, delta: 1 | -1) => {
|
||||
const existing = getVoteForThread(id);
|
||||
const next = existing === delta ? 0 : delta;
|
||||
setVoteForThread(id, next as 1 | -1 | 0);
|
||||
setVoteBump((v) => v + 1);
|
||||
};
|
||||
|
||||
const visible = selectedCat === "All"
|
||||
? threads
|
||||
: threads.filter((t) => t.category === selectedCat || t.topicSlug === selectedCat.toLowerCase());
|
||||
|
||||
return (
|
||||
<div className="glass rounded-3xl border border-white/10 p-8">
|
||||
<div className="mb-10">
|
||||
<h2 className="font-orbitron text-4xl font-bold">DARKNET FORUM</h2>
|
||||
<p className="mt-2 text-foreground/70">Encrypted, anonymous discussions. No logs, no traces.</p>
|
||||
<div className="mb-8 flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="font-orbitron text-3xl font-bold">VOID AGGREGATE</h2>
|
||||
<p className="mt-1 text-sm text-foreground/60">
|
||||
{threads.length} threads shown · full board at{" "}
|
||||
<Link href="/forum" className="text-neon-cyan hover:underline">/forum</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Category filter */}
|
||||
<div className="mb-8 flex flex-wrap gap-3">
|
||||
{categories.map((cat) => (
|
||||
<div className="mb-6 flex flex-wrap gap-2">
|
||||
{CATEGORIES.map((cat) => (
|
||||
<button
|
||||
key={cat}
|
||||
className={`rounded-full px-4 py-2 text-sm font-medium transition-all ${selectedCategory === cat
|
||||
type="button"
|
||||
onClick={() => setSelectedCat(cat)}
|
||||
className={`rounded-full px-4 py-1.5 text-xs font-medium transition-all ${
|
||||
selectedCat === cat
|
||||
? "bg-gradient-to-r from-neon-cyan to-neon-purple text-background"
|
||||
: "glass border border-white/10"
|
||||
}`}
|
||||
onClick={() => setSelectedCategory(cat)}
|
||||
: "glass border border-white/10 hover:border-neon-cyan/30"
|
||||
}`}
|
||||
>
|
||||
{cat}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* New post form */}
|
||||
<form onSubmit={handleSubmit} className="mb-10">
|
||||
<div className="glass rounded-2xl border border-white/10 p-6">
|
||||
{/* Quick post form */}
|
||||
<form onSubmit={handleSubmit} className="mb-8">
|
||||
<div className="glass rounded-2xl border border-white/10 p-5">
|
||||
<textarea
|
||||
className="w-full bg-transparent text-foreground placeholder-foreground/50 focus:outline-none"
|
||||
rows={3}
|
||||
placeholder="Type your encrypted message... (all posts are ephemeral)"
|
||||
className="w-full bg-transparent text-sm text-foreground placeholder-foreground/40 focus:outline-none"
|
||||
rows={2}
|
||||
placeholder={user ? `Post as @${user.username}…` : "Quick post (stored in your browser)…"}
|
||||
value={newPost}
|
||||
onChange={(e) => setNewPost(e.target.value)}
|
||||
maxLength={500}
|
||||
/>
|
||||
<div className="mt-4 flex items-center justify-between">
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-2 rounded-full bg-white/5 px-4 py-2 text-sm"
|
||||
>
|
||||
<span>🔒</span> Encrypt
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-2 rounded-full bg-white/5 px-4 py-2 text-sm"
|
||||
>
|
||||
<span>🕵️</span> Post Anonymously
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-3 flex items-center justify-between">
|
||||
<span className="text-[10px] text-foreground/30">{newPost.length}/500 · local storage</span>
|
||||
<button
|
||||
type="submit"
|
||||
className="rounded-full bg-gradient-to-r from-neon-cyan to-neon-purple px-6 py-3 font-bold text-background"
|
||||
disabled={!newPost.trim() || posting}
|
||||
className="rounded-full bg-gradient-to-r from-neon-cyan to-neon-purple px-5 py-2 text-sm font-bold text-background disabled:opacity-40"
|
||||
>
|
||||
PUBLISH
|
||||
Post
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Posts list */}
|
||||
<div className="space-y-6">
|
||||
{filteredPosts.map((post) => (
|
||||
<div key={post.id} className="glass rounded-2xl border border-white/10 p-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-gradient-to-br from-neon-cyan to-neon-purple text-2xl">
|
||||
{post.avatar}
|
||||
</div>
|
||||
<div>
|
||||
{/* Thread list */}
|
||||
<div className="space-y-4">
|
||||
{visible.length === 0 ? (
|
||||
<p className="py-6 text-center text-sm text-foreground/40">No threads in this category yet.</p>
|
||||
) : (
|
||||
visible.map((t) => {
|
||||
const score = displayScore(t);
|
||||
const voted = typeof window !== "undefined" ? getVoteForThread(t.id) : 0;
|
||||
return (
|
||||
<div key={t.id + voteBump} className="glass rounded-2xl border border-white/10 p-5">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="font-bold">{post.user}</span>
|
||||
<span className="rounded-full bg-white/5 px-3 py-1 text-xs">{post.category}</span>
|
||||
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-neon-cyan/20 to-neon-purple/20 text-sm font-bold">
|
||||
{(t.author ?? "?").charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-sm font-bold">@{t.author ?? "anon"}</span>
|
||||
{t.category && (
|
||||
<span className="rounded-full bg-white/5 px-2 py-0.5 text-[10px]">{t.category}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-foreground/40">{new Date(t.ts).toLocaleDateString()}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-foreground/60">{post.timestamp}</div>
|
||||
<div className="flex shrink-0 items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleVote(t.id, 1)}
|
||||
className={`text-xs transition-colors ${voted === 1 ? "text-neon-cyan" : "text-foreground/40 hover:text-neon-cyan"}`}
|
||||
>
|
||||
▲ {score}
|
||||
</button>
|
||||
<span className="text-xs text-foreground/30">💬 {t.replies.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<Link href="/forum" className="text-sm hover:text-neon-cyan/80">
|
||||
{t.title ?? t.body?.slice(0, 100)}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<button className="flex items-center gap-2 text-sm text-foreground/60 hover:text-neon-cyan">
|
||||
<span>⬆</span> {post.upvotes}
|
||||
</button>
|
||||
<button className="flex items-center gap-2 text-sm text-foreground/60 hover:text-neon-cyan">
|
||||
<span>💬</span> {post.replies}
|
||||
</button>
|
||||
<button className="rounded-full bg-white/5 p-2 hover:bg-white/10">
|
||||
<span>🔗</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<p>{post.content}</p>
|
||||
</div>
|
||||
<div className="mt-6 flex items-center gap-6 border-t border-white/10 pt-6">
|
||||
<button className="flex items-center gap-2 text-sm">
|
||||
<span>⬆</span> Upvote
|
||||
</button>
|
||||
<button className="flex items-center gap-2 text-sm">
|
||||
<span>⬇</span> Downvote
|
||||
</button>
|
||||
<button className="flex items-center gap-2 text-sm">
|
||||
<span>💬</span> Reply
|
||||
</button>
|
||||
<button className="flex items-center gap-2 text-sm">
|
||||
<span>🔐</span> Encrypt Reply
|
||||
</button>
|
||||
<button className="flex items-center gap-2 text-sm">
|
||||
<span>🚀</span> Boost
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Forum stats */}
|
||||
<div className="mt-10 grid grid-cols-2 gap-6 md:grid-cols-4">
|
||||
<div className="glass rounded-2xl border border-white/10 p-6 text-center">
|
||||
<div className="text-3xl font-bold text-neon-cyan">2.4k</div>
|
||||
<div className="text-sm text-foreground/60">Active Users</div>
|
||||
</div>
|
||||
<div className="glass rounded-2xl border border-white/10 p-6 text-center">
|
||||
<div className="text-3xl font-bold text-neon-purple">18.5k</div>
|
||||
<div className="text-sm text-foreground/60">Total Posts</div>
|
||||
</div>
|
||||
<div className="glass rounded-2xl border border-white/10 p-6 text-center">
|
||||
<div className="text-3xl font-bold text-neon-pink">94%</div>
|
||||
<div className="text-sm text-foreground/60">Encrypted</div>
|
||||
</div>
|
||||
<div className="glass rounded-2xl border border-white/10 p-6 text-center">
|
||||
<div className="text-3xl font-bold text-neon-green">0</div>
|
||||
<div className="text-sm text-foreground/60">Data Leaks</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 text-center text-xs text-foreground/40">
|
||||
<p>
|
||||
Preview board — for threads that persist in your browser, open the{" "}
|
||||
<Link href="/forum" className="text-neon-cyan hover:underline">
|
||||
full forum
|
||||
</Link>{" "}
|
||||
(dedicated .onion maps here too).
|
||||
</p>
|
||||
<div className="mt-6 text-center">
|
||||
<Link
|
||||
href="/forum"
|
||||
className="inline-flex items-center gap-2 rounded-full border border-neon-cyan/30 px-6 py-2 text-sm font-bold text-neon-cyan hover:bg-neon-cyan/10"
|
||||
>
|
||||
Open full forum →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ForumBoard;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useCart } from "@/contexts/CartContext";
|
||||
|
||||
const Navbar = () => {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const { isConnected, address, connect, disconnect, luxCredits, usdStoreCredit } = useWallet();
|
||||
const { luxCredits, usdStoreCredit } = useWallet();
|
||||
const { user, hydrated } = useAccount();
|
||||
const { itemCount, hydrated: cartReady } = useCart();
|
||||
|
||||
@@ -26,17 +26,10 @@ const Navbar = () => {
|
||||
{ label: "Syndicate", href: "/syndicate", icon: "🕸️" },
|
||||
{ label: "Void crawl", href: "/search", icon: "🔦" },
|
||||
{ label: "Mirrors", href: "/account/hidden-services", icon: "🧅" },
|
||||
{ label: "Add funds", href: "/account/add-funds", icon: "₿" },
|
||||
{ label: "Dark web launch", href: "/launch", icon: "🔥" },
|
||||
];
|
||||
|
||||
const handleWalletClick = () => {
|
||||
if (isConnected) {
|
||||
disconnect();
|
||||
} else {
|
||||
connect();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="glass fixed top-0 left-0 right-0 z-50 mx-auto mt-4 max-w-7xl rounded-2xl border border-white/10 px-6 py-4 backdrop-blur-xl">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -104,12 +97,20 @@ const Navbar = () => {
|
||||
</div>
|
||||
{hydrated ? (
|
||||
user ? (
|
||||
<Link
|
||||
href="/dashboard"
|
||||
className="hidden items-center gap-2 rounded-full border border-neon-cyan/30 bg-neon-cyan/10 px-4 py-2 text-sm font-medium text-neon-cyan hover:bg-neon-cyan/20 sm:flex"
|
||||
>
|
||||
<span className="max-w-[8rem] truncate">{user.displayName}</span>
|
||||
</Link>
|
||||
<>
|
||||
<Link
|
||||
href="/account/add-funds"
|
||||
className="hidden rounded-full border border-neon-green/35 bg-neon-green/10 px-3 py-2 text-xs font-bold uppercase tracking-wide text-neon-green hover:bg-neon-green/15 sm:inline-flex"
|
||||
>
|
||||
Add funds
|
||||
</Link>
|
||||
<Link
|
||||
href="/dashboard"
|
||||
className="hidden items-center gap-2 rounded-full border border-neon-cyan/30 bg-neon-cyan/10 px-4 py-2 text-sm font-medium text-neon-cyan hover:bg-neon-cyan/20 sm:flex"
|
||||
>
|
||||
<span className="max-w-[8rem] truncate">{user.displayName}</span>
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Link
|
||||
@@ -127,16 +128,6 @@ const Navbar = () => {
|
||||
</>
|
||||
)
|
||||
) : null}
|
||||
<button
|
||||
onClick={handleWalletClick}
|
||||
className={`flex items-center gap-2 rounded-full px-4 py-2 font-medium transition-all ${isConnected
|
||||
? "bg-gradient-to-r from-neon-green to-neon-cyan text-background"
|
||||
: "glow-border text-neon-cyan hover:bg-white/5"
|
||||
}`}
|
||||
>
|
||||
<span className="text-lg">{isConnected ? "🟢" : "⚡"}</span>
|
||||
{isConnected ? `${address?.slice(0, 6)}...${address?.slice(-4)}` : "Connect Wallet"}
|
||||
</button>
|
||||
<button
|
||||
className="rounded-full bg-white/5 p-2 hover:bg-white/10"
|
||||
aria-label="Toggle menu"
|
||||
|
||||
Reference in New Issue
Block a user