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" },
|
||||
|
||||
@@ -3,37 +3,46 @@
|
||||
import Link from "next/link";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
|
||||
/** Total time for the progress bar to reach 100% (ms). */
|
||||
const DURATION_MS = 3500;
|
||||
/** Hard cap — always complete after this (ms) even if something glitches. */
|
||||
const MAX_WAIT_MS = 9000;
|
||||
/** Wall-clock polling (Tor Browser throttles requestAnimationFrame aggressively). */
|
||||
const TICK_MS = 50;
|
||||
|
||||
export default function DDoSProtection({ onComplete }: { onComplete: () => void }) {
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [status, setStatus] = useState("Checking your browser…");
|
||||
/** Client-only — avoids SSR/client HTML mismatch from Math.random() in JSX. */
|
||||
const [rayId, setRayId] = useState("");
|
||||
const doneRef = useRef(false);
|
||||
const onCompleteRef = useRef(onComplete);
|
||||
onCompleteRef.current = onComplete;
|
||||
|
||||
const finish = () => {
|
||||
if (doneRef.current) return;
|
||||
doneRef.current = true;
|
||||
setProgress(100);
|
||||
setStatus("Access granted.");
|
||||
window.setTimeout(() => onCompleteRef.current(), 400);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setRayId(Math.random().toString(36).substring(2, 10).toUpperCase());
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setProgress((prev) => {
|
||||
if (prev >= 100) {
|
||||
return 100;
|
||||
}
|
||||
const next = prev + Math.random() * 15;
|
||||
if (next >= 100) {
|
||||
if (!doneRef.current) {
|
||||
doneRef.current = true;
|
||||
clearInterval(interval);
|
||||
setTimeout(() => onCompleteRef.current(), 500);
|
||||
}
|
||||
return 100;
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, 400);
|
||||
const start = Date.now();
|
||||
doneRef.current = false;
|
||||
|
||||
const progressInterval = window.setInterval(() => {
|
||||
const elapsed = Date.now() - start;
|
||||
const pct = Math.min(100, (elapsed / DURATION_MS) * 100);
|
||||
setProgress(pct);
|
||||
if (pct >= 100 || elapsed >= MAX_WAIT_MS) {
|
||||
window.clearInterval(progressInterval);
|
||||
finish();
|
||||
}
|
||||
}, TICK_MS);
|
||||
|
||||
const statusUpdates = [
|
||||
"Checking your browser…",
|
||||
@@ -43,20 +52,24 @@ export default function DDoSProtection({ onComplete }: { onComplete: () => void
|
||||
"Establishing encrypted tunnel…",
|
||||
"Access granted.",
|
||||
];
|
||||
|
||||
let statusIndex = 0;
|
||||
const statusInterval = setInterval(() => {
|
||||
const statusInterval = window.setInterval(() => {
|
||||
if (statusIndex < statusUpdates.length - 1) {
|
||||
statusIndex++;
|
||||
setStatus(statusUpdates[statusIndex]);
|
||||
setStatus(statusUpdates[statusIndex]!);
|
||||
} else {
|
||||
clearInterval(statusInterval);
|
||||
}
|
||||
}, 800);
|
||||
}, 650);
|
||||
|
||||
const maxTimer = window.setTimeout(() => {
|
||||
if (!doneRef.current) finish();
|
||||
}, MAX_WAIT_MS);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
window.clearInterval(progressInterval);
|
||||
clearInterval(statusInterval);
|
||||
clearTimeout(maxTimer);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -69,25 +82,36 @@ export default function DDoSProtection({ onComplete }: { onComplete: () => void
|
||||
className="w-full max-w-md border-2 border-[#00ff41]/20 bg-[#050505] p-12 shadow-[0_0_50px_rgba(0,255,65,0.1)]"
|
||||
style={{ backgroundColor: "#050505", borderColor: "rgba(0,255,65,0.2)" }}
|
||||
>
|
||||
<div className="text-4xl mb-8 animate-pulse">🛡️</div>
|
||||
<h1 className="text-2xl font-bold mb-2 uppercase tracking-tighter">DDoS Protection</h1>
|
||||
<p className="text-xs opacity-60 mb-8 leading-relaxed">
|
||||
<div className="mb-8 animate-pulse text-4xl">🛡️</div>
|
||||
<h1 className="mb-2 text-2xl font-bold uppercase tracking-tighter">DDoS Protection</h1>
|
||||
<p className="mb-8 text-xs leading-relaxed opacity-60">
|
||||
Origin shield active — wait while we validate your session. Automated requests are throttled at the edge.
|
||||
</p>
|
||||
|
||||
<div className="w-full h-2 bg-[#00ff41]/10 mb-4 overflow-hidden">
|
||||
<div className="mb-4 h-2 w-full overflow-hidden bg-[#00ff41]/10">
|
||||
<div
|
||||
className="h-full bg-[#00ff41] transition-all duration-300 ease-out"
|
||||
style={{ width: `${progress}%` }}
|
||||
className="h-full bg-[#00ff41] transition-[width] duration-100 ease-linear"
|
||||
style={{ width: `${Math.min(100, progress)}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between text-[10px] uppercase tracking-widest">
|
||||
<span>{status}</span>
|
||||
<span>{Math.floor(progress)}%</span>
|
||||
<span>{Math.floor(Math.min(100, progress))}%</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 text-[8px] opacity-20 leading-relaxed">
|
||||
<button
|
||||
type="button"
|
||||
onClick={finish}
|
||||
className="mt-8 w-full border border-[#00ff41]/40 bg-[#00ff41]/10 py-3 text-xs font-bold uppercase tracking-wider text-[#00ff41] transition hover:bg-[#00ff41]/20"
|
||||
>
|
||||
Continue to site
|
||||
</button>
|
||||
<p className="mt-2 text-center text-[9px] text-[#00ff41]/40">
|
||||
Stuck on Tor or slow device? Tap above — no challenge required.
|
||||
</p>
|
||||
|
||||
<div className="mt-8 text-[8px] leading-relaxed opacity-20">
|
||||
Ray ID: {rayId || "—"}
|
||||
<br />
|
||||
Performance & Security by ShadowGuard™
|
||||
|
||||
@@ -32,7 +32,7 @@ const Hero = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className="relative min-h-screen overflow-hidden px-4 pt-32 md:pt-40">
|
||||
<section className="relative min-h-[85vh] overflow-hidden px-4 pt-24 md:min-h-screen md:pt-28">
|
||||
{/* Animated background particles */}
|
||||
<div className="absolute inset-0 -z-20">
|
||||
{particles.map((p, i) => (
|
||||
@@ -69,25 +69,25 @@ const Hero = () => {
|
||||
|
||||
<div className="container relative mx-auto max-w-6xl">
|
||||
{/* Glitch text effect */}
|
||||
<div className="relative mb-6">
|
||||
<h1 className="font-orbitron text-5xl font-black leading-tight text-foreground md:text-7xl lg:text-8xl">
|
||||
<span className="block">REVOLUTIONARY</span>
|
||||
<div className="relative mb-5">
|
||||
<h1 className="font-orbitron text-3xl font-black leading-[1.1] text-foreground sm:text-4xl md:text-5xl lg:text-6xl">
|
||||
<span className="block">FORBIDDEN</span>
|
||||
<span className="block text-transparent bg-gradient-to-r from-neon-cyan via-neon-purple to-neon-pink bg-clip-text">
|
||||
E‑COMMERCE
|
||||
DARK ARTS
|
||||
</span>
|
||||
<span className="block">EXPERIENCE</span>
|
||||
<span className="block">EMPORIUM</span>
|
||||
</h1>
|
||||
<div className="absolute -top-2 left-0 -z-10 text-5xl font-black text-neon-cyan opacity-30 blur-md md:text-7xl lg:text-8xl">
|
||||
REVOLUTIONARY
|
||||
<div className="pointer-events-none absolute -top-1 left-0 -z-10 text-3xl font-black text-neon-cyan opacity-20 blur-sm sm:text-4xl md:text-5xl lg:text-6xl">
|
||||
FORBIDDEN
|
||||
</div>
|
||||
<div className="absolute -bottom-2 right-0 -z-10 text-5xl font-black text-neon-purple opacity-30 blur-md md:text-7xl lg:text-8xl">
|
||||
EXPERIENCE
|
||||
<div className="pointer-events-none absolute -bottom-1 right-0 -z-10 text-3xl font-black text-neon-purple opacity-20 blur-sm sm:text-4xl md:text-5xl lg:text-6xl">
|
||||
EMPORIUM
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="mb-8 max-w-2xl text-xl text-foreground/80 md:text-2xl">
|
||||
Curated <span className="text-neon-cyan">gray-market candy</span> storefront with live catalog, vendor hall, and BTC-settled
|
||||
checkout — plus companion onions for forums, listings, and the wiki rail.
|
||||
<p className="mb-6 max-w-2xl text-base text-foreground/80 md:text-lg">
|
||||
Curated <span className="text-neon-cyan">mystical artifact</span> apothecary with live catalog, alchemist guilds, and soul-bound
|
||||
checkout — plus companion realms for covenants, incantations, and the forbidden wiki.
|
||||
</p>
|
||||
|
||||
<div className="mb-10 flex flex-wrap gap-2 font-mono text-[10px] uppercase tracking-[0.2em] text-foreground/45">
|
||||
@@ -143,18 +143,18 @@ const Hero = () => {
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="mt-20 grid grid-cols-2 gap-6 sm:grid-cols-4">
|
||||
<div className="mt-12 grid grid-cols-2 gap-4 sm:mt-16 sm:grid-cols-4">
|
||||
{[
|
||||
{ value: String(SHOP_PRODUCT_COUNT), label: "Live SKUs", color: "text-neon-cyan" },
|
||||
{ value: String(SHOP_SELLER_COUNT), label: "Vendor stalls", color: "text-neon-green" },
|
||||
{ value: "₿ · Ξ · XMR", label: "Settlement rails", color: "text-neon-purple" },
|
||||
{ value: "v3 ring", label: "Tor-ready layout", color: "text-neon-pink" },
|
||||
{ value: String(SHOP_PRODUCT_COUNT), label: "Arcane Relics", color: "text-neon-cyan" },
|
||||
{ value: String(SHOP_SELLER_COUNT), label: "Alchemist Guilds", color: "text-neon-green" },
|
||||
{ value: "Mana · Aether", label: "Soul bindings", color: "text-neon-purple" },
|
||||
{ value: "v3 coven", label: "Shadow routing", color: "text-neon-pink" },
|
||||
].map((stat) => (
|
||||
<div
|
||||
key={stat.label}
|
||||
className="glass rounded-2xl border border-white/10 p-6 backdrop-blur-sm"
|
||||
>
|
||||
<div className={`font-orbitron text-3xl font-bold ${stat.color}`}>{stat.value}</div>
|
||||
<div className={`font-orbitron text-xl font-bold sm:text-2xl ${stat.color}`}>{stat.value}</div>
|
||||
<div className="mt-2 text-sm text-foreground/60">{stat.label}</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -1,98 +1,134 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { useWallet } from "@/contexts/WalletContext";
|
||||
import { useAccount } from "@/contexts/AccountContext";
|
||||
import { useCart } from "@/contexts/CartContext";
|
||||
import { SITE_NAV_GROUPS } from "@/lib/siteNav";
|
||||
|
||||
const Navbar = () => {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const [pagesOpen, setPagesOpen] = useState(false);
|
||||
const pagesDropdownRef = useRef<HTMLDivElement>(null);
|
||||
const { luxCredits, usdStoreCredit } = useWallet();
|
||||
const { user, hydrated } = useAccount();
|
||||
const { itemCount, hydrated: cartReady } = useCart();
|
||||
|
||||
const navItems = [
|
||||
{ label: "Sanctuary", href: "/sanctuary", icon: "🛡️" },
|
||||
{ label: "Market", href: "/market", icon: "📈" },
|
||||
{ label: "Vendors", href: "/vendors", icon: "🏪" },
|
||||
{ label: "Checkout", href: "/checkout", icon: "₿" },
|
||||
{ label: "Forum", href: "/forum", icon: "◆" },
|
||||
{ label: "Exchange", href: "/exchange", icon: "📰" },
|
||||
{ label: "Barter", href: "/barter", icon: "♻️" },
|
||||
{ label: "Vault", href: "/vault", icon: "🗝️" },
|
||||
{ label: "Wiki", href: "/hidden-wiki", icon: "📚" },
|
||||
{ label: "Atlas", href: "/darknet-atlas", icon: "🗺️" },
|
||||
{ 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: "🔥" },
|
||||
];
|
||||
useEffect(() => {
|
||||
const onDoc = (e: MouseEvent) => {
|
||||
if (!pagesDropdownRef.current?.contains(e.target as Node)) setPagesOpen(false);
|
||||
};
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") setPagesOpen(false);
|
||||
};
|
||||
document.addEventListener("mousedown", onDoc);
|
||||
document.addEventListener("keydown", onKey);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", onDoc);
|
||||
document.removeEventListener("keydown", onKey);
|
||||
};
|
||||
}, []);
|
||||
|
||||
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">
|
||||
{/* Logo */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative">
|
||||
<div className="h-10 w-10 rounded-full bg-gradient-to-br from-neon-cyan to-neon-purple p-0.5">
|
||||
<div className="h-full w-full rounded-full bg-background flex items-center justify-center">
|
||||
<span className="text-xl font-bold">⏣</span>
|
||||
<nav className="glass fixed top-0 left-0 right-0 z-50 mx-auto mt-2 max-w-7xl rounded-xl border border-white/10 px-4 py-2 backdrop-blur-xl md:px-5">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2 md:gap-3">
|
||||
<Link href="/" className="flex shrink-0 items-center gap-2">
|
||||
<div className="relative h-8 w-8 shrink-0 rounded-full bg-gradient-to-br from-neon-cyan to-neon-purple p-0.5">
|
||||
<div className="flex h-full w-full items-center justify-center rounded-full bg-background">
|
||||
<span className="text-sm font-bold">⏣</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute -inset-1 -z-10 animate-pulse rounded-full bg-neon-cyan blur-md opacity-30"></div>
|
||||
</div>
|
||||
<Link href="/" className="font-orbitron text-2xl font-bold tracking-tighter">
|
||||
Cyber<span className="text-neon-cyan">Lux</span>
|
||||
<span className="font-orbitron truncate text-base font-bold tracking-tight md:text-lg">
|
||||
Eldritch<span className="text-neon-cyan">Weave</span>
|
||||
</span>
|
||||
</Link>
|
||||
<span className="hidden rounded-full bg-muted px-3 py-1 text-xs font-medium text-neon-green sm:inline">
|
||||
v1.0 • ALPHA
|
||||
<span className="hidden rounded-full bg-muted/80 px-2 py-0.5 text-[10px] font-medium text-neon-green/90 sm:inline">
|
||||
v1 · spellcraft
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<div className="hidden max-w-lg flex-wrap items-center justify-end gap-x-3 gap-y-1 md:flex lg:max-w-2xl lg:gap-x-4 xl:max-w-none xl:flex-nowrap xl:gap-x-5">
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.label}
|
||||
href={item.href}
|
||||
className="group relative text-sm font-medium text-foreground/80 transition-colors hover:text-neon-cyan"
|
||||
<div ref={pagesDropdownRef} className="relative ml-1 hidden md:block">
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={pagesOpen}
|
||||
aria-haspopup="true"
|
||||
onClick={() => setPagesOpen((o) => !o)}
|
||||
className="flex items-center gap-1 rounded-lg border border-white/15 bg-white/5 px-3 py-1.5 text-xs font-semibold text-foreground/85 transition hover:border-neon-cyan/35 hover:text-neon-cyan"
|
||||
>
|
||||
<span className="mr-1 opacity-60">{item.icon}</span>
|
||||
{item.label}
|
||||
<span className="absolute -bottom-1 left-0 h-0.5 w-0 bg-gradient-to-r from-neon-cyan to-neon-purple transition-all group-hover:w-full"></span>
|
||||
</Link>
|
||||
))}
|
||||
All pages
|
||||
<span className="text-[10px] opacity-70">{pagesOpen ? "▴" : "▾"}</span>
|
||||
</button>
|
||||
{pagesOpen ? (
|
||||
<div className="absolute left-0 top-full z-[60] mt-1 max-h-[min(70vh,520px)] w-[min(100vw-2rem,380px)] overflow-y-auto rounded-xl border border-white/15 bg-[#0a0a0f]/95 p-3 shadow-2xl shadow-black/50 backdrop-blur-xl">
|
||||
{SITE_NAV_GROUPS.map((group) => (
|
||||
<div key={group.title} className="mb-3 last:mb-0">
|
||||
<p className="mb-1.5 px-1 font-mono text-[9px] font-bold uppercase tracking-[0.2em] text-foreground/40">
|
||||
{group.title}
|
||||
</p>
|
||||
<ul className="space-y-0.5">
|
||||
{group.items.map((item) => (
|
||||
<li key={item.href}>
|
||||
<Link
|
||||
href={item.href}
|
||||
className="flex items-center gap-2 rounded-lg px-2 py-1.5 text-xs text-foreground/80 transition hover:bg-white/10 hover:text-neon-cyan"
|
||||
onClick={() => setPagesOpen(false)}
|
||||
>
|
||||
<span className="w-5 text-center opacity-70">{item.icon}</span>
|
||||
<span className="truncate">{item.label}</span>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href="/market"
|
||||
className="hidden text-xs font-medium text-foreground/70 transition hover:text-neon-cyan lg:inline"
|
||||
>
|
||||
Market
|
||||
</Link>
|
||||
<Link
|
||||
href="/checkout"
|
||||
className="hidden text-xs font-medium text-foreground/70 transition hover:text-neon-purple lg:inline"
|
||||
>
|
||||
Checkout
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-3 md:gap-4">
|
||||
<div className="flex shrink-0 items-center gap-2 md:gap-3">
|
||||
{cartReady ? (
|
||||
<Link
|
||||
href="/checkout"
|
||||
className="relative hidden rounded-full border border-white/10 px-3 py-2 text-sm text-foreground/80 hover:border-neon-cyan/40 hover:text-neon-cyan sm:inline-flex"
|
||||
className="relative inline-flex rounded-full border border-white/10 px-2.5 py-1.5 text-xs text-foreground/80 hover:border-neon-cyan/40 hover:text-neon-cyan"
|
||||
title="Cart"
|
||||
>
|
||||
🛒
|
||||
{itemCount > 0 ? (
|
||||
<span className="absolute -right-1 -top-1 flex h-5 min-w-5 items-center justify-center rounded-full bg-neon-pink px-1 text-[10px] font-bold text-background">
|
||||
<span className="absolute -right-1 -top-1 flex h-4 min-w-4 items-center justify-center rounded-full bg-neon-pink px-0.5 text-[9px] font-bold text-background">
|
||||
{itemCount > 99 ? "99+" : itemCount}
|
||||
</span>
|
||||
) : null}
|
||||
</Link>
|
||||
) : null}
|
||||
<div className="hidden md:flex items-center gap-3">
|
||||
<div className="flex items-center gap-2 rounded-full bg-white/5 px-4 py-2 border border-white/10">
|
||||
<span className="text-xs text-foreground/60">LUX</span>
|
||||
<span className="font-orbitron text-sm text-neon-green">{luxCredits.toLocaleString()}</span>
|
||||
<div className="hidden items-center gap-1.5 sm:flex">
|
||||
<div
|
||||
className="flex items-center gap-1 rounded-full border border-white/10 bg-white/5 px-2 py-1 text-[10px]"
|
||||
title="LUX credits"
|
||||
>
|
||||
<span className="text-foreground/50">LUX</span>
|
||||
<span className="font-orbitron text-[11px] text-neon-green">{luxCredits.toLocaleString()}</span>
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center gap-2 rounded-full bg-white/5 px-4 py-2 border border-neon-cyan/20"
|
||||
title="Verified Bitcoin deposit balance (USD)"
|
||||
className="flex items-center gap-1 rounded-full border border-neon-cyan/20 bg-white/5 px-2 py-1 text-[10px]"
|
||||
title="Bitcoin-funded USD balance"
|
||||
>
|
||||
<span className="text-xs text-foreground/60">USD</span>
|
||||
<span className="font-orbitron text-sm text-neon-cyan">${usdStoreCredit.toFixed(2)}</span>
|
||||
<span className="text-foreground/50">$</span>
|
||||
<span className="font-orbitron text-[11px] text-neon-cyan">{usdStoreCredit.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
{hydrated ? (
|
||||
@@ -100,28 +136,28 @@ const Navbar = () => {
|
||||
<>
|
||||
<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"
|
||||
className="hidden rounded-full border border-neon-green/30 bg-neon-green/10 px-2.5 py-1 text-[10px] font-bold uppercase tracking-wide text-neon-green hover:bg-neon-green/15 sm:inline-flex"
|
||||
>
|
||||
Add funds
|
||||
+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"
|
||||
className="hidden max-w-[6rem] truncate rounded-full border border-neon-cyan/25 bg-neon-cyan/10 px-2.5 py-1 text-[11px] font-medium text-neon-cyan hover:bg-neon-cyan/18 sm:inline"
|
||||
>
|
||||
<span className="max-w-[8rem] truncate">{user.displayName}</span>
|
||||
{user.displayName}
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Link
|
||||
href="/sign-in"
|
||||
className="hidden rounded-full border border-white/15 px-4 py-2 text-sm font-medium text-foreground/80 hover:border-neon-cyan/50 hover:text-neon-cyan sm:inline-flex"
|
||||
className="hidden rounded-full border border-white/12 px-2.5 py-1 text-[11px] font-medium text-foreground/80 hover:border-neon-cyan/45 hover:text-neon-cyan sm:inline-flex"
|
||||
>
|
||||
Sign in
|
||||
</Link>
|
||||
<Link
|
||||
href="/sign-up"
|
||||
className="hidden rounded-full border border-neon-purple/35 bg-neon-purple/10 px-4 py-2 text-sm font-medium text-neon-purple hover:bg-neon-purple/20 md:inline-flex"
|
||||
className="hidden rounded-full border border-neon-purple/30 bg-neon-purple/10 px-2.5 py-1 text-[11px] font-medium text-neon-purple hover:bg-neon-purple/18 md:inline-flex"
|
||||
>
|
||||
Register
|
||||
</Link>
|
||||
@@ -129,41 +165,39 @@ const Navbar = () => {
|
||||
)
|
||||
) : null}
|
||||
<button
|
||||
className="rounded-full bg-white/5 p-2 hover:bg-white/10"
|
||||
className="rounded-full bg-white/5 p-1.5 hover:bg-white/10 md:hidden"
|
||||
aria-label="Toggle menu"
|
||||
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
||||
>
|
||||
<div className="h-5 w-5 space-y-1.5">
|
||||
<div className={`h-0.5 bg-neon-cyan transition-transform ${isMenuOpen ? "translate-y-2 rotate-45" : ""}`}></div>
|
||||
<div className={`h-0.5 bg-neon-purple transition-opacity ${isMenuOpen ? "opacity-0" : ""}`}></div>
|
||||
<div className={`h-0.5 bg-neon-pink transition-transform ${isMenuOpen ? "-translate-y-2 -rotate-45" : ""}`}></div>
|
||||
<div className="h-4 w-4 space-y-1">
|
||||
<div className={`h-0.5 bg-neon-cyan transition-transform ${isMenuOpen ? "translate-y-1.5 rotate-45" : ""}`} />
|
||||
<div className={`h-0.5 bg-neon-purple transition-opacity ${isMenuOpen ? "opacity-0" : ""}`} />
|
||||
<div className={`h-0.5 bg-neon-pink transition-transform ${isMenuOpen ? "-translate-y-1.5 -rotate-45" : ""}`} />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu */}
|
||||
{/* Mobile: full menu + page list */}
|
||||
{isMenuOpen && (
|
||||
<div className="glass mt-4 rounded-xl border border-white/10 p-4 backdrop-blur-xl md:hidden">
|
||||
<div className="mb-3 flex gap-2 border-b border-white/10 pb-3">
|
||||
<div className="glass mt-2 max-h-[75vh] overflow-y-auto rounded-lg border border-white/10 p-3 backdrop-blur-xl md:hidden">
|
||||
<div className="mb-2 flex flex-wrap gap-2 border-b border-white/10 pb-2">
|
||||
{cartReady ? (
|
||||
<Link
|
||||
href="/checkout"
|
||||
className="relative flex-1 rounded-lg border border-white/15 py-2 text-center text-sm font-medium"
|
||||
className="relative flex-1 rounded-lg border border-white/15 py-2 text-center text-xs font-medium"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
🛒 Cart
|
||||
{itemCount > 0 ? (
|
||||
<span className="ml-1 rounded bg-neon-pink px-1.5 text-[10px] font-bold text-background">
|
||||
{itemCount}
|
||||
</span>
|
||||
<span className="ml-1 rounded bg-neon-pink px-1 text-[9px] font-bold text-background">{itemCount}</span>
|
||||
) : null}
|
||||
</Link>
|
||||
) : null}
|
||||
{hydrated && user ? (
|
||||
<Link
|
||||
href="/dashboard"
|
||||
className="flex-1 rounded-lg bg-neon-cyan/15 py-2 text-center text-sm font-bold text-neon-cyan"
|
||||
className="flex-1 rounded-lg bg-neon-cyan/15 py-2 text-center text-xs font-bold text-neon-cyan"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
Dashboard
|
||||
@@ -171,55 +205,35 @@ const Navbar = () => {
|
||||
) : hydrated ? (
|
||||
<Link
|
||||
href="/sign-in"
|
||||
className="flex-1 rounded-lg border border-white/15 py-2 text-center text-sm font-medium"
|
||||
className="flex-1 rounded-lg border border-white/15 py-2 text-center text-xs font-medium"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
Sign in
|
||||
</Link>
|
||||
) : null}
|
||||
{hydrated && !user ? (
|
||||
<Link
|
||||
href="/sign-up"
|
||||
className="flex-1 rounded-lg border border-neon-purple/40 py-2 text-center text-sm text-neon-purple"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
Register
|
||||
</Link>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.label}
|
||||
href={item.href}
|
||||
className="flex items-center gap-2 rounded-lg bg-white/5 p-3 text-sm font-medium transition-colors hover:bg-white/10"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
<span className="text-lg">{item.icon}</span>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-4 flex gap-3 border-t border-white/10 pt-4 text-xs">
|
||||
<Link
|
||||
href="/sanctuary"
|
||||
className="flex-1 rounded-lg border border-white/10 px-3 py-2 text-center text-foreground/70 hover:border-neon-cyan/40 hover:text-neon-cyan"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
Sanctuary
|
||||
</Link>
|
||||
<Link
|
||||
href="/account/hidden-services"
|
||||
className="flex-1 rounded-lg border border-white/10 px-3 py-2 text-center text-foreground/70 hover:border-neon-purple/40 hover:text-neon-purple"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
Onion map
|
||||
</Link>
|
||||
</div>
|
||||
{SITE_NAV_GROUPS.map((group) => (
|
||||
<div key={group.title} className="mb-3">
|
||||
<p className="mb-1 font-mono text-[9px] font-bold uppercase tracking-widest text-foreground/35">{group.title}</p>
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
{group.items.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="flex items-center gap-1.5 rounded-md bg-white/5 p-2 text-[11px] font-medium"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
<span>{item.icon}</span>
|
||||
<span className="truncate">{item.label}</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
export default Navbar;
|
||||
export default Navbar;
|
||||
|
||||
111
components/PayWithUsdBalanceButton.tsx
Normal file
111
components/PayWithUsdBalanceButton.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useAccount } from "@/contexts/AccountContext";
|
||||
import { useWallet } from "@/contexts/WalletContext";
|
||||
|
||||
type Props = {
|
||||
/** USD amount to charge (2 decimals) */
|
||||
amountUsd: number;
|
||||
/** Receipt / vault title */
|
||||
title: string;
|
||||
description?: string;
|
||||
className?: string;
|
||||
size?: "sm" | "md";
|
||||
/** Override LUX granted (default: based on USD amount) */
|
||||
luxBonus?: number;
|
||||
/** Called after a successful charge */
|
||||
onSuccess?: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Spend verified Bitcoin-funded USD balance without going through full checkout cart.
|
||||
*/
|
||||
export default function PayWithUsdBalanceButton({
|
||||
amountUsd,
|
||||
title,
|
||||
description,
|
||||
className = "",
|
||||
size = "md",
|
||||
luxBonus,
|
||||
onSuccess,
|
||||
}: Props) {
|
||||
const { user } = useAccount();
|
||||
const { usdStoreCredit, spendUsdStoreCredit, earnLuxCredits, addVaultReceipt } = useWallet();
|
||||
const [msg, setMsg] = useState<{ kind: "ok" | "err"; text: string } | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const safe = Math.round(amountUsd * 100) / 100;
|
||||
const pad = size === "sm" ? "px-3 py-1.5 text-xs" : "px-5 py-2.5 text-sm";
|
||||
|
||||
const pay = () => {
|
||||
setMsg(null);
|
||||
if (!user) {
|
||||
setMsg({ kind: "err", text: "Sign in to spend your USD balance." });
|
||||
return;
|
||||
}
|
||||
if (!(safe > 0)) {
|
||||
setMsg({ kind: "err", text: "Invalid amount." });
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
try {
|
||||
if (!spendUsdStoreCredit(safe)) {
|
||||
setMsg({
|
||||
kind: "err",
|
||||
text: `Need $${safe.toFixed(2)} — you have $${usdStoreCredit.toFixed(2)}. Add funds first.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const lux =
|
||||
luxBonus != null && Number.isFinite(luxBonus)
|
||||
? Math.max(0, Math.floor(luxBonus))
|
||||
: Math.min(500, Math.max(10, Math.floor(safe)));
|
||||
earnLuxCredits(lux);
|
||||
const oid =
|
||||
typeof crypto !== "undefined" && "randomUUID" in crypto
|
||||
? `PAY-${crypto.randomUUID().slice(0, 8).toUpperCase()}`
|
||||
: `PAY-${Date.now().toString(36).toUpperCase()}`;
|
||||
addVaultReceipt({
|
||||
id: oid,
|
||||
title,
|
||||
desc: description ?? `${title} · $${safe.toFixed(2)} USD`,
|
||||
date: new Date().toISOString().slice(0, 10),
|
||||
severity: "normal",
|
||||
});
|
||||
setMsg({ kind: "ok", text: `Paid $${safe.toFixed(2)}. +${lux} LUX · receipt ${oid}` });
|
||||
onSuccess?.();
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy || safe <= 0}
|
||||
onClick={pay}
|
||||
className={`rounded-full border border-neon-cyan/40 bg-neon-cyan/15 font-bold text-neon-cyan transition hover:bg-neon-cyan/25 disabled:opacity-40 ${pad} ${className}`}
|
||||
>
|
||||
{busy ? "Processing…" : `Pay $${safe.toFixed(2)} with balance`}
|
||||
</button>
|
||||
{!user ? (
|
||||
<p className="text-xs text-foreground/55">
|
||||
<Link href="/sign-in" className="text-neon-cyan underline">
|
||||
Sign in
|
||||
</Link>{" "}
|
||||
to use BTC-funded USD credit.
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-xs text-foreground/45">
|
||||
Balance: <span className="font-orbitron text-neon-green">${usdStoreCredit.toFixed(2)}</span>
|
||||
</p>
|
||||
)}
|
||||
{msg ? (
|
||||
<p className={`text-xs ${msg.kind === "ok" ? "text-neon-green" : "text-red-400"}`}>{msg.text}</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
46
components/market/MarketCommerceBar.tsx
Normal file
46
components/market/MarketCommerceBar.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useWallet } from "@/contexts/WalletContext";
|
||||
import { useCart } from "@/contexts/CartContext";
|
||||
import { useAccount } from "@/contexts/AccountContext";
|
||||
|
||||
export default function MarketCommerceBar() {
|
||||
const { usdStoreCredit, luxCredits } = useWallet();
|
||||
const { itemCount, hydrated } = useCart();
|
||||
const { user, hydrated: accReady } = useAccount();
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center justify-end gap-2 border-b border-[#00ff41]/15 bg-[#040805]/90 px-3 py-2 font-mono text-[10px] text-[#8fccb0]">
|
||||
<span className="text-[#5a8f6a]">Spendable</span>
|
||||
<span title="Bitcoin-funded USD">
|
||||
USD <strong className="text-[#7af598]">${usdStoreCredit.toFixed(2)}</strong>
|
||||
</span>
|
||||
<span className="text-[#00ff41]/25">|</span>
|
||||
<span title="LUX credits">
|
||||
LUX <strong className="text-[#b4ffcc]">{luxCredits.toLocaleString()}</strong>
|
||||
</span>
|
||||
{accReady && !user ? (
|
||||
<>
|
||||
<span className="text-[#00ff41]/25">|</span>
|
||||
<Link href="/sign-in?next=/market" className="text-[#7af598] underline hover:text-white">
|
||||
Sign in to pay
|
||||
</Link>
|
||||
</>
|
||||
) : null}
|
||||
<span className="text-[#00ff41]/25">|</span>
|
||||
<Link
|
||||
href="/checkout"
|
||||
className="inline-flex items-center gap-1 rounded border border-[#00ff41]/35 px-2 py-1 text-[#c8ffd8] hover:border-[#00ff41]/60"
|
||||
>
|
||||
🛒 Checkout
|
||||
{hydrated && itemCount > 0 ? (
|
||||
<span className="rounded bg-[#ff00aa]/90 px-1 text-[9px] font-bold text-black">{itemCount}</span>
|
||||
) : null}
|
||||
</Link>
|
||||
<Link href="/account/add-funds" className="rounded border border-[#00ff41]/20 px-2 py-1 hover:border-[#00ff41]/45">
|
||||
+Funds
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import MarketCommerceBar from "@/components/market/MarketCommerceBar";
|
||||
import LexiconStrip from "@/components/site/LexiconStrip";
|
||||
import { MARKET_ATMOSPHERE_TAGS } from "@/lib/cyberluxZoneLexicon";
|
||||
import { SHOP_PRODUCT_COUNT } from "@/lib/shopCatalog";
|
||||
@@ -70,15 +71,18 @@ export default function MarketSiteChrome({ children }: { children: React.ReactNo
|
||||
/>
|
||||
|
||||
<header className="sticky top-0 z-30 border-b border-[#00ff41]/20 bg-[#030806]/95 backdrop-blur-md">
|
||||
<div className="mx-auto flex max-w-[1600px] flex-col gap-3 px-4 py-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div className="mx-auto max-w-[1600px]">
|
||||
<MarketCommerceBar />
|
||||
</div>
|
||||
<div className="mx-auto flex max-w-[1600px] flex-col gap-3 px-4 py-3 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<p className="font-mono text-[9px] uppercase tracking-[0.5em] text-[#00ff41]/45">sovereign catalog · multi-layer</p>
|
||||
<Link href="/market" className="mt-1 block">
|
||||
<h1 className="font-mono text-2xl font-black tracking-tight text-[#7af598] md:text-3xl">
|
||||
<h1 className="font-mono text-xl font-black tracking-tight text-[#7af598] md:text-2xl">
|
||||
The Candy Shop
|
||||
</h1>
|
||||
</Link>
|
||||
<p className="mt-1 max-w-xl font-mono text-[11px] text-[#00ff41]/55">
|
||||
<p className="mt-1 max-w-xl font-mono text-[10px] text-[#00ff41]/55 md:text-[11px]">
|
||||
<strong className="text-[#9dffc4]">{SHOP_PRODUCT_COUNT}</strong> SKUs ·{" "}
|
||||
<strong className="text-[#9dffc4]">{SHOP_SELLER_COUNT}</strong> stalls — not a plugin mall; one cart, one checkout, four trust layers.
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user