Update market, account, and onion operations

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

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

59
app/account/page.tsx Normal file
View File

@@ -0,0 +1,59 @@
"use client";
import Link from "next/link";
import { useAccount } from "@/contexts/AccountContext";
export default function AccountHubPage() {
const { user, hydrated } = useAccount();
return (
<div className="min-h-screen bg-[#0a0a0a] px-4 py-14 text-zinc-200">
<div className="mx-auto max-w-lg">
<p className="font-mono text-[10px] uppercase tracking-[0.35em] text-emerald-500/80">account</p>
<h1 className="mt-3 font-orbitron text-2xl font-bold text-zinc-100">Session & mirrors</h1>
<p className="mt-4 text-sm leading-relaxed text-zinc-500">
This host stores handles, balances, and forum keys in your browser. Use the same links on any CyberLux
.onion import a bundle on each hostname if you want one identity everywhere.
</p>
<ul className="mt-8 space-y-2 text-sm text-zinc-300">
<li>
<Link href="/dashboard" className="text-emerald-400 hover:underline">
Dashboard
</Link>{" "}
activity, profile, vendor queue
</li>
<li>
<Link href="/account/add-funds" className="text-emerald-400 hover:underline">
Add funds
</Link>{" "}
Bitcoin USD store credit
</li>
<li>
<Link href="/account/hidden-services" className="text-emerald-400 hover:underline">
Onion mirror map
</Link>{" "}
export / import identity bundle
</li>
<li>
<Link href="/sign-in" className="text-emerald-400 hover:underline">
Sign in
</Link>{" "}
·{" "}
<Link href="/sign-up" className="text-emerald-400 hover:underline">
Sign up
</Link>
</li>
</ul>
{hydrated && user ? (
<p className="mt-8 font-mono text-xs text-zinc-500">
Signed in as <span className="text-emerald-200/90">@{user.username}</span>
</p>
) : hydrated ? (
<p className="mt-8 text-xs text-zinc-600">Not signed in on this hostname.</p>
) : null}
</div>
</div>
);
}

79
app/api/messages/route.ts Normal file
View File

@@ -0,0 +1,79 @@
import { NextResponse } from "next/server";
import { promises as fs } from "fs";
import path from "path";
const MESSAGES_FILE = path.join(process.cwd(), ".messages.json");
type Msg = { id: string; from: string; text: string; ts: number; channel?: string };
async function getMessages(): Promise<Msg[]> {
try {
const data = await fs.readFile(MESSAGES_FILE, "utf-8");
return JSON.parse(data) as Msg[];
} catch {
return [];
}
}
async function saveMessages(msgs: Msg[]) {
const toSave = msgs.slice(-500);
await fs.writeFile(MESSAGES_FILE, JSON.stringify(toSave, null, 2), "utf-8");
}
export async function GET(req: Request) {
const url = new URL(req.url);
const channel = url.searchParams.get("channel") || "global";
const msgs = await getMessages();
const channelMsgs = msgs.filter((m) => m.channel === channel || channel === "all");
return NextResponse.json({ ok: true, messages: channelMsgs });
}
export async function POST(req: Request) {
try {
const body = await req.json();
const { from, text, channel = "global" } = body;
if (!from || !text) {
return NextResponse.json({ ok: false, error: "Missing fields" }, { status: 400 });
}
const newMsg: Msg = {
id: Math.random().toString(36).slice(2, 10),
from: from.trim(),
text: text.trim().slice(0, 500),
ts: Date.now(),
channel,
};
const msgs = await getMessages();
msgs.push(newMsg);
await saveMessages(msgs);
return NextResponse.json({ ok: true, message: newMsg });
} catch (err) {
return NextResponse.json({ ok: false, error: "Failed to save message" }, { status: 500 });
}
}
export async function DELETE(req: Request) {
try {
const url = new URL(req.url);
const id = url.searchParams.get("id");
const clearAll = url.searchParams.get("all") === "true";
let msgs = await getMessages();
if (clearAll) {
msgs = [];
} else if (id) {
msgs = msgs.filter((m) => m.id !== id);
} else {
return NextResponse.json({ ok: false, error: "Provide id or all=true" }, { status: 400 });
}
await saveMessages(msgs);
return NextResponse.json({ ok: true });
} catch (err) {
return NextResponse.json({ ok: false, error: "Failed to delete" }, { status: 500 });
}
}

View File

@@ -1,22 +1,22 @@
@import "tailwindcss";
:root {
--background: #0a0a0a;
--foreground: #f0f0f0;
--primary: #8b00ff;
--secondary: #00e0ff;
--background: #030008;
--foreground: #e6ffed;
--primary: #00ff66;
--secondary: #9000ff;
--accent: #ff0055;
--muted: #1a1a1a;
--card: rgba(20, 20, 30, 0.8);
--glass: rgba(255, 255, 255, 0.05);
--glass-strong: rgba(255, 255, 255, 0.075);
--ring: rgba(0, 255, 255, 0.35);
--shadow-soft: 0 18px 60px rgba(0, 0, 0, 0.55);
--shadow-glass: 0 10px 40px rgba(0, 0, 0, 0.35);
--neon-cyan: #00ffff;
--neon-purple: #9d00ff;
--neon-pink: #ff00ff;
--neon-green: #00ff9d;
--muted: #0d1210;
--card: rgba(5, 10, 5, 0.85);
--glass: rgba(0, 255, 100, 0.04);
--glass-strong: rgba(144, 0, 255, 0.08);
--ring: rgba(0, 255, 102, 0.35);
--shadow-soft: 0 18px 60px rgba(0, 255, 102, 0.15);
--shadow-glass: 0 10px 40px rgba(144, 0, 255, 0.15);
--neon-cyan: #00ff66;
--neon-purple: #9000ff;
--neon-pink: #ff0066;
--neon-green: #b3ff00;
}
@theme inline {
@@ -65,11 +65,11 @@ body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-image:
radial-gradient(1200px 700px at 15% 20%, rgba(157, 0, 255, 0.18) 0%, transparent 55%),
radial-gradient(900px 600px at 85% 65%, rgba(0, 255, 255, 0.16) 0%, transparent 55%),
radial-gradient(700px 450px at 65% 15%, rgba(255, 0, 255, 0.10) 0%, transparent 50%),
radial-gradient(900px 700px at 30% 85%, rgba(0, 255, 157, 0.10) 0%, transparent 55%),
linear-gradient(180deg, rgba(255,255,255,0.03), transparent 55%);
radial-gradient(1200px 700px at 15% 20%, rgba(144, 0, 255, 0.18) 0%, transparent 55%),
radial-gradient(900px 600px at 85% 65%, rgba(0, 255, 102, 0.14) 0%, transparent 55%),
radial-gradient(700px 450px at 65% 15%, rgba(255, 0, 102, 0.10) 0%, transparent 50%),
radial-gradient(900px 700px at 30% 85%, rgba(179, 255, 0, 0.12) 0%, transparent 55%),
linear-gradient(180deg, rgba(0, 255, 102, 0.03), transparent 55%);
background-attachment: fixed;
}

View File

@@ -137,7 +137,7 @@ const ENTRIES: { cat: string; items: WikiItem[] }[] = [
items: [
{
title: "Launch / deploy notes",
note: "Tor + nginx operator entry.",
note: "Tor + nginx + verify script — operator entry for every .onion on this host.",
href: "/launch",
external: false,
},
@@ -227,6 +227,12 @@ export default function HiddenWikiPage() {
>
All mirrors
</Link>
<Link
href="/launch"
className="border border-[#2a3444] bg-[#0f141c] px-3 py-1.5 text-[#94a8b8] hover:border-[#3d5a80]/50"
>
Launch
</Link>
<Link
href="/syndicate"
className="border border-[#2a3444] bg-[#0f141c] px-3 py-1.5 text-[#94a8b8] hover:border-[#3d5a80]/50"

View File

@@ -18,10 +18,10 @@ const rajdhani = Rajdhani({
export const metadata: Metadata = {
title: {
default: "CyberLux",
template: "%s · CyberLux",
default: "EldritchWeave",
template: "%s · EldritchWeave",
},
description: "Curated storefront, catalog, forums, and Tor companion rails.",
description: "Curated apothecary, grimoires, covenants, and shadow rails.",
};
export default function RootLayout({ children }: { children: React.ReactNode }) {

View File

@@ -3,6 +3,7 @@
import Link from "next/link";
import { useState } from "react";
import ThemedLayout from "@/components/layouts/ThemedLayout";
import { LINK_GARDEN_INTERNAL } from "@/lib/cyberluxSitemap";
type Resource = {
name: string;
@@ -89,19 +90,6 @@ const RESOURCES: Resource[] = [
},
];
const INTERNAL: { name: string; description: string; href: string; icon: string }[] = [
{ name: "Darknet Atlas", description: "Analyst taxonomy of underground ecosystems — every row links to a real CyberLux surface.", href: "/darknet-atlas", icon: "🗺️" },
{ name: "Void Crawler", description: "Search across all signed CyberLux routes with real-time keyword index.", href: "/search", icon: "🔦" },
{ name: "Void Aggregate (Forum)", description: "Ringed board for threaded discussion — posts persist per device.", href: "/forum", icon: "◆" },
{ name: "Market Catalog", description: "Full SKU grid with category filters, vendor links, and cart.", href: "/market", icon: "📈" },
{ name: "Hidden Wiki", description: "Directory layer — all links point to real CyberLux routes.", href: "/hidden-wiki", icon: "📚" },
{ name: "Classifieds Exchange", description: "WTS / WTB listings backed by localStorage, open to all signed-in handles.", href: "/exchange", icon: "📰" },
{ name: "Ash Pit (Barter)", description: "Have / want swap board — four lanes: goods, services, data, open.", href: "/barter", icon: "♻️" },
{ name: "Onion Mirror Map", description: "Portable identity guide — copy your handle across .onion hostnames.", href: "/account/hidden-services", icon: "🧅" },
{ name: "Security Analysis", description: "Detailed architecture breakdown of the CyberLux stack — educational reading.", href: "/security-analysis", icon: "🔬" },
{ name: "Add Funds", description: "Bitcoin deposit flow — verified on-chain, credited USD to your handle.", href: "/account/add-funds", icon: "₿" },
];
const TRUST_LABELS: Record<Resource["trust"], { label: string; color: string }> = {
internal: { label: "Internal", color: "text-neon-green" },
clearnet: { label: "Clearnet", color: "text-neon-cyan" },
@@ -185,7 +173,7 @@ export default function LinksPage() {
<section className="container mx-auto max-w-6xl px-4 py-16">
<h2 className="mb-8 font-orbitron text-3xl font-bold">INTERNAL ROUTES</h2>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{INTERNAL.map((item) => (
{LINK_GARDEN_INTERNAL.map((item) => (
<Link
key={item.href}
href={item.href}

View File

@@ -1,5 +1,16 @@
import { Suspense } from "react";
import MarketSiteChrome from "@/components/market/MarketSiteChrome";
export default function MarketLayout({ children }: { children: React.ReactNode }) {
return <MarketSiteChrome>{children}</MarketSiteChrome>;
return (
<MarketSiteChrome>
<Suspense
fallback={
<div className="py-16 text-center font-mono text-sm text-[#00ff41]/45">Loading catalog</div>
}
>
{children}
</Suspense>
</MarketSiteChrome>
);
}

View File

@@ -2,7 +2,8 @@
import Image from "next/image";
import Link from "next/link";
import { useMemo, useState } from "react";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useSearchParams } from "next/navigation";
import { ProductSocialBlock } from "@/components/shop/ProductSocialBlock";
import { useCart } from "@/contexts/CartContext";
@@ -14,6 +15,7 @@ import {
type ShopCurrency,
type ShopProduct,
} from "@/lib/shopCatalog";
import { estimateUsdForCryptoAmount } from "@/lib/shopFx";
import { getProductSocialMetrics } from "@/lib/shopProductSocial";
import { SHOP_SELLER_COUNT, getSellerById } from "@/lib/shopSellers";
@@ -31,56 +33,129 @@ const SYM: Record<ShopCurrency, string> = {
XMR: "⏣",
};
type SortKey = "relevance" | "price-asc" | "price-desc" | "name";
export default function MarketPage() {
const sp = useSearchParams();
const router = useRouter();
const initialCat = sp.get("category") ?? "";
const [q, setQ] = useState("");
const [category, setCategory] = useState(initialCat);
const [sort, setSort] = useState<SortKey>("relevance");
const [currencyFilter, setCurrencyFilter] = useState<ShopCurrency | "">("");
const [btcUsd, setBtcUsd] = useState<number | null>(null);
const facet = useMemo(() => shopCategoryCounts(), []);
useEffect(() => {
let cancelled = false;
void (async () => {
try {
const res = await fetch(
"https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd",
{ cache: "no-store" },
);
const j = (await res.json()) as { bitcoin?: { usd?: number } };
if (!cancelled && j.bitcoin?.usd && Number.isFinite(j.bitcoin.usd)) setBtcUsd(j.bitcoin.usd);
} catch {
if (!cancelled) setBtcUsd(96000);
}
})();
return () => {
cancelled = true;
};
}, []);
const spotBtc = btcUsd ?? 96000;
const filtered = useMemo(() => {
const ql = q.trim().toLowerCase();
const tokens = ql.split(/\s+/).filter(Boolean);
return SHOP_PRODUCTS.filter((p) => {
if (category && p.category !== category) return false;
if (currencyFilter && p.currency !== currencyFilter) return false;
if (tokens.length === 0) return true;
const seller = getSellerById(p.sellerId);
const sellerHay = seller ? `${seller.handle} ${seller.bio} ${seller.specialty}` : "";
const hay = `${p.name} ${p.description} ${p.category} ${p.id} ${sellerHay}`.toLowerCase();
return tokens.every((t) => hay.includes(t));
});
}, [q, category]);
}, [q, category, currencyFilter]);
const sorted = useMemo(() => {
const list = [...filtered];
const unitUsd = (p: ShopProduct) => estimateUsdForCryptoAmount(p.price, p.currency, spotBtc);
switch (sort) {
case "price-asc":
return list.sort((a, b) => unitUsd(a) - unitUsd(b));
case "price-desc":
return list.sort((a, b) => unitUsd(b) - unitUsd(a));
case "name":
return list.sort((a, b) => a.name.localeCompare(b.name));
default:
return list;
}
}, [filtered, sort, spotBtc]);
return (
<div className="font-mono text-[#00ff41]">
<section className="mb-8 border-b border-[#00ff41]/20 pb-6">
<section className="mb-6 border-b border-[#00ff41]/20 pb-4">
<p className="text-[10px] uppercase tracking-[0.4em] text-[#00ff41]/50">Layer 1 · live floor</p>
<p className="mt-2 max-w-3xl text-sm text-[#00ff41]/65">
<strong className="text-[#7af598]">{SHOP_PRODUCT_COUNT}</strong> SKUs ·{" "}
<Link href="/vendors" className="font-bold text-[#9dffc4] underline hover:text-white">
{SHOP_SELLER_COUNT} vendor stalls
</Link>
per-listing reviews, velocity, and cart handoff into checkout (USD estimate from spot). Use the left rail for ops, trust, and intel layers.
. Every SKU has a detail page with qty, cart, checkout, or instant USD balance payment. Spot:{" "}
<span className="text-[#7af598]">${spotBtc.toLocaleString(undefined, { maximumFractionDigits: 0 })}/BTC</span>.
</p>
</section>
<div className="mb-6 flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
<div className="mb-4 flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
<div className="flex-1">
<label className="text-[10px] uppercase tracking-widest text-[#00ff41]/50">Search catalog</label>
<input
value={q}
onChange={(e) => setQ(e.target.value)}
placeholder="tokens match title, description, category…"
className="mt-1 w-full max-w-xl border-2 border-[#00ff41]/30 bg-black/80 px-4 py-3 text-sm text-[#c8ffd8] placeholder:text-[#00ff41]/25 focus:border-[#00ff41] focus:outline-none"
placeholder="title, description, vendor…"
className="mt-1 w-full max-w-xl border-2 border-[#00ff41]/30 bg-black/80 px-4 py-2.5 text-sm text-[#c8ffd8] placeholder:text-[#00ff41]/25 focus:border-[#00ff41] focus:outline-none"
/>
</div>
<p className="text-xs text-[#00ff41]/50">
Showing <span className="text-[#7af598]">{filtered.length}</span> / {SHOP_PRODUCT_COUNT}
</p>
<div className="flex flex-wrap items-center gap-3 text-xs text-[#00ff41]/50">
<span>
Showing <span className="text-[#7af598]">{sorted.length}</span> / {SHOP_PRODUCT_COUNT}
</span>
<label className="flex items-center gap-2">
<span className="text-[10px] uppercase">Sort</span>
<select
value={sort}
onChange={(e) => setSort(e.target.value as SortKey)}
className="border border-[#00ff41]/35 bg-black px-2 py-1 text-[11px] text-[#c8ffd8]"
>
<option value="relevance">Default</option>
<option value="price-asc">Price (USD est.)</option>
<option value="price-desc">Price (USD est.)</option>
<option value="name">Name AZ</option>
</select>
</label>
<label className="flex items-center gap-2">
<span className="text-[10px] uppercase">Currency</span>
<select
value={currencyFilter}
onChange={(e) => setCurrencyFilter((e.target.value || "") as ShopCurrency | "")}
className="border border-[#00ff41]/35 bg-black px-2 py-1 text-[11px] text-[#c8ffd8]"
>
<option value="">All</option>
<option value="BTC">BTC</option>
<option value="ETH">ETH</option>
<option value="USD">USD</option>
<option value="XMR">XMR</option>
</select>
</label>
</div>
</div>
<div className="mb-8 flex flex-wrap gap-2">
<div className="mb-6 flex flex-wrap gap-2">
<button
type="button"
onClick={() => setCategory("")}
@@ -102,12 +177,12 @@ export default function MarketPage() {
</div>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
{filtered.map((p) => (
<MarketProductCard key={p.id} product={p} />
{sorted.map((p) => (
<MarketProductCard key={p.id} product={p} spotBtc={spotBtc} onBuyNow={() => router.push("/checkout")} />
))}
</div>
{filtered.length === 0 ? (
{sorted.length === 0 ? (
<p className="mt-16 border border-dashed border-[#00ff41]/20 py-12 text-center text-sm text-[#00ff41]/50">
No SKUs match widen search or pick another category.
</p>
@@ -116,17 +191,37 @@ export default function MarketPage() {
);
}
function MarketProductCard({ product: p }: { product: ShopProduct }) {
function MarketProductCard({
product: p,
spotBtc,
onBuyNow,
}: {
product: ShopProduct;
spotBtc: number;
onBuyNow: () => void;
}) {
const src = resolveProductImage(p);
const local = src.startsWith("/");
const v = getSellerById(p.sellerId);
const metrics = getProductSocialMetrics(p.id);
const { addToCart, hydrated } = useCart();
const [flash, setFlash] = useState(false);
const unitUsd = estimateUsdForCryptoAmount(p.price, p.currency, spotBtc);
const add = () => {
addToCart(p, 1);
setFlash(true);
window.setTimeout(() => setFlash(false), 1400);
};
const buyNow = () => {
addToCart(p, 1);
onBuyNow();
};
return (
<article className="flex h-full flex-col border border-[#00ff41]/20 bg-[#0d0d0d] transition-all hover:border-[#00ff41]/55">
<div className="relative aspect-square w-full bg-black">
<Link href={`/market/product/${encodeURIComponent(p.id)}`} className="relative block aspect-square w-full bg-black">
<Image
src={src}
alt=""
@@ -138,10 +233,12 @@ function MarketProductCard({ product: p }: { product: ShopProduct }) {
{p.limited ? (
<span className="absolute left-2 top-2 bg-[#ff00aa] px-2 py-0.5 text-[10px] font-bold uppercase text-black">Limited</span>
) : null}
</div>
</Link>
<div className="flex flex-1 flex-col p-4">
<p className="text-[10px] uppercase tracking-wider text-[#00ff41]/45">{p.category}</p>
<h2 className="mt-1 text-base font-bold leading-snug text-[#d8ffe8]">{p.name}</h2>
<Link href={`/market/product/${encodeURIComponent(p.id)}`}>
<h2 className="mt-1 text-base font-bold leading-snug text-[#d8ffe8] hover:text-[#7af598]">{p.name}</h2>
</Link>
<div className="mt-2 text-[#9dffc4]">
<ProductSocialBlock metrics={metrics} compact />
</div>
@@ -154,30 +251,39 @@ function MarketProductCard({ product: p }: { product: ShopProduct }) {
<span className="text-[#00ff41]/35"> · stall {v.rating.toFixed(2)}</span>
</p>
) : null}
<p className="mt-2 line-clamp-3 text-xs leading-relaxed text-[#00ff41]/70">{p.description}</p>
<blockquote className="mt-3 border-l-2 border-[#00ff41]/30 pl-3 text-[11px] italic leading-snug text-[#b4ffcc]/80">
{metrics.testimonials[0]?.text}
<span className="mt-0.5 block font-mono text-[10px] not-italic text-[#00ff41]/45">
@{metrics.testimonials[0]?.handle} · {metrics.testimonials[0]?.daysAgo}d
</span>
</blockquote>
<div className="mt-4 flex flex-wrap items-center justify-between gap-2 border-t border-[#00ff41]/15 pt-3">
<span className="text-lg font-bold text-[#7af598]">
{SYM[p.currency]}
{formatPrice(p.price, p.currency)} <span className="text-xs font-normal text-[#00ff41]/50">{p.currency}</span>
</span>
<p className="mt-2 line-clamp-2 text-xs leading-relaxed text-[#00ff41]/70">{p.description}</p>
<div className="mt-3 flex items-baseline justify-between gap-2 border-t border-[#00ff41]/15 pt-3">
<div>
<span className="text-lg font-bold text-[#7af598]">
{SYM[p.currency]}
{formatPrice(p.price, p.currency)} <span className="text-xs font-normal text-[#00ff41]/50">{p.currency}</span>
</span>
<p className="font-mono text-[10px] text-[#5a8f6a]"> ${unitUsd.toFixed(2)} USD</p>
</div>
<Link
href={`/market/product/${encodeURIComponent(p.id)}`}
className="shrink-0 text-[10px] font-bold uppercase text-[#7af598] underline hover:text-white"
>
Details
</Link>
</div>
<div className="mt-3 flex flex-wrap gap-2">
<button
type="button"
disabled={!hydrated}
onClick={() => {
addToCart(p, 1);
setFlash(true);
window.setTimeout(() => setFlash(false), 1400);
}}
className="border border-[#00ff41] bg-[#00ff41] px-3 py-1.5 text-xs font-bold uppercase text-black hover:bg-[#7af598] disabled:opacity-50"
onClick={add}
className="flex-1 border border-[#00ff41] bg-[#00ff41] px-2 py-1.5 text-[10px] font-bold uppercase text-black hover:bg-[#7af598] disabled:opacity-50"
>
{flash ? "Added" : "Add"}
</button>
<button
type="button"
disabled={!hydrated}
onClick={buyNow}
className="flex-1 border border-[#00ff41]/40 px-2 py-1.5 text-[10px] font-bold uppercase text-[#7af598] hover:bg-[#00ff41]/10 disabled:opacity-50"
>
Buy now
</button>
</div>
</div>
</article>

View File

@@ -0,0 +1,220 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import { useParams, useRouter } from "next/navigation";
import { useCallback, useEffect, useMemo, useState } from "react";
import { ProductSocialBlock } from "@/components/shop/ProductSocialBlock";
import PayWithUsdBalanceButton from "@/components/PayWithUsdBalanceButton";
import { useCart } from "@/contexts/CartContext";
import {
getShopProductById,
resolveProductImage,
type ShopCurrency,
} from "@/lib/shopCatalog";
import { estimateUsdForCryptoAmount } from "@/lib/shopFx";
import { getProductSocialMetrics } from "@/lib/shopProductSocial";
import { getSellerById } from "@/lib/shopSellers";
const SYM: Record<ShopCurrency, string> = {
BTC: "₿",
ETH: "Ξ",
USD: "$",
XMR: "⏣",
};
function formatPrice(price: number, currency: ShopCurrency): string {
if (currency === "BTC") return price.toFixed(6);
if (currency === "ETH") return price.toFixed(4);
if (currency === "XMR") return price.toFixed(3);
return price.toFixed(2);
}
export default function MarketProductPage() {
const params = useParams();
const id = typeof params?.id === "string" ? params.id : "";
const router = useRouter();
const product = id ? getShopProductById(id) : undefined;
const [btcUsd, setBtcUsd] = useState<number | null>(null);
const [qty, setQty] = useState(1);
const [addedFlash, setAddedFlash] = useState(false);
const { addToCart, hydrated } = useCart();
useEffect(() => {
let cancelled = false;
void (async () => {
try {
const res = await fetch(
"https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd",
{ cache: "no-store" },
);
const j = (await res.json()) as { bitcoin?: { usd?: number } };
if (!cancelled && j.bitcoin?.usd && Number.isFinite(j.bitcoin.usd)) setBtcUsd(j.bitcoin.usd);
} catch {
if (!cancelled) setBtcUsd(96000);
}
})();
return () => {
cancelled = true;
};
}, []);
const spot = btcUsd ?? 96000;
const lineUsd = useMemo(() => {
if (!product) return 0;
const unit = estimateUsdForCryptoAmount(product.price, product.currency, spot);
return Math.round(unit * qty * 100) / 100;
}, [product, qty, spot]);
const buyNow = useCallback(() => {
if (!product || !hydrated) return;
addToCart(product, qty);
router.push("/checkout");
}, [product, qty, addToCart, hydrated, router]);
const addOnly = useCallback(() => {
if (!product || !hydrated) return;
addToCart(product, qty);
setAddedFlash(true);
window.setTimeout(() => setAddedFlash(false), 1200);
}, [product, qty, addToCart, hydrated]);
if (!product) {
return (
<div className="border border-dashed border-[#00ff41]/25 py-20 text-center font-mono text-sm text-[#00ff41]/55">
<p>SKU not found.</p>
<Link href="/market" className="mt-4 inline-block text-[#7af598] underline">
Back to catalog
</Link>
</div>
);
}
const src = resolveProductImage(product);
const local = src.startsWith("/");
const v = getSellerById(product.sellerId);
const metrics = getProductSocialMetrics(product.id);
const unitUsd = estimateUsdForCryptoAmount(product.price, product.currency, spot);
return (
<div className="font-mono text-[#c8ffd8]">
<nav className="mb-6 text-[10px] uppercase tracking-wider text-[#5a8f6a]">
<Link href="/market" className="text-[#7af598] hover:underline">
Catalog
</Link>
<span className="mx-2 text-[#00ff41]/25">/</span>
<span className="text-[#00ff41]/55">{product.category}</span>
</nav>
<div className="grid gap-8 lg:grid-cols-2">
<div className="relative aspect-square w-full overflow-hidden border border-[#00ff41]/20 bg-black">
<Image
src={src}
alt=""
fill
className="object-cover"
sizes="(max-width: 1024px) 100vw,50vw"
unoptimized={local}
/>
{product.limited ? (
<span className="absolute left-2 top-2 bg-[#ff00aa] px-2 py-0.5 text-[10px] font-bold uppercase text-black">
Limited
</span>
) : null}
</div>
<div>
<p className="text-[10px] uppercase tracking-[0.3em] text-[#00ff41]/45">{product.category}</p>
<h1 className="mt-2 font-mono text-2xl font-bold leading-tight text-[#e8fff0] md:text-3xl">{product.name}</h1>
<div className="mt-3 text-[#9dffc4]">
<ProductSocialBlock metrics={metrics} compact={false} />
</div>
{v ? (
<p className="mt-3 text-[11px] text-[#00ff41]/55">
Seller{" "}
<Link href={`/vendor/${v.slug}`} className="text-[#7af598] hover:underline">
{v.handle}
</Link>
<span className="text-[#00ff41]/35"> · {v.rating.toFixed(2)}</span>
</p>
) : null}
<p className="mt-4 text-sm leading-relaxed text-[#00ff41]/75">{product.description}</p>
<div className="mt-6 border border-[#00ff41]/20 bg-[#0a120d] p-4">
<div className="flex flex-wrap items-end justify-between gap-4">
<div>
<p className="text-[10px] uppercase text-[#5a8f6a]">List price</p>
<p className="text-2xl font-bold text-[#7af598]">
{SYM[product.currency]}
{formatPrice(product.price, product.currency)}{" "}
<span className="text-sm font-normal text-[#00ff41]/45">{product.currency}</span>
</p>
<p className="mt-1 text-[11px] text-[#5a8f6a]">
${unitUsd.toFixed(2)} USD / unit @ spot
</p>
</div>
<div className="text-right">
<p className="text-[10px] uppercase text-[#5a8f6a]">Line total (est.)</p>
<p className="text-xl font-bold text-[#b4ffcc]">${lineUsd.toFixed(2)} USD</p>
</div>
</div>
<div className="mt-4 flex flex-wrap items-center gap-3">
<span className="text-[10px] uppercase text-[#5a8f6a]">Qty</span>
<button
type="button"
className="border border-[#00ff41]/35 px-2 py-1 text-sm hover:bg-[#00ff41]/10"
onClick={() => setQty(Math.max(1, qty - 1))}
>
</button>
<span className="w-8 text-center font-mono">{qty}</span>
<button
type="button"
className="border border-[#00ff41]/35 px-2 py-1 text-sm hover:bg-[#00ff41]/10"
onClick={() => setQty(Math.min(99, qty + 1))}
>
+
</button>
</div>
<div className="mt-6 flex flex-col gap-4 border-t border-[#00ff41]/15 pt-4">
<div className="flex flex-col gap-2 sm:flex-row">
<button
type="button"
disabled={!hydrated}
onClick={addOnly}
className="flex-1 border border-[#00ff41]/40 bg-transparent px-4 py-3 text-xs font-bold uppercase text-[#7af598] hover:bg-[#00ff41]/10 disabled:opacity-50"
>
{addedFlash ? "Added ✓" : "Add to cart"}
</button>
<button
type="button"
disabled={!hydrated}
onClick={buyNow}
className="flex-1 border border-[#00ff41] bg-[#00ff41] px-4 py-3 text-xs font-bold uppercase text-black hover:bg-[#7af598] disabled:opacity-50"
>
Buy now checkout
</button>
</div>
<p className="text-[10px] text-[#5a8f6a]">
Checkout converts every line to USD using live BTC spot, then charges your verified Bitcoin-funded USD balance.
</p>
<div className="rounded border border-[#00ff41]/20 bg-black/40 p-3">
<p className="mb-2 text-[10px] font-bold uppercase text-[#7af598]">Express · pay from USD balance</p>
<PayWithUsdBalanceButton
amountUsd={lineUsd}
title={`Market: ${product.name}`}
description={`Qty ${qty} · ${product.id}`}
size="sm"
/>
</div>
</div>
</div>
</div>
</div>
</div>
);
}

View File

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

View File

@@ -25,17 +25,17 @@ export default function Home() {
<Hero />
{/* Featured Products */}
<section id="featured" className="container mx-auto max-w-6xl scroll-mt-28 px-4 py-20">
<section id="featured" className="container mx-auto max-w-6xl scroll-mt-24 px-4 py-20">
<div className="mb-12 flex flex-col gap-8 lg:flex-row lg:items-end lg:justify-between">
<div className="max-w-xl">
<p className="mb-2 font-mono text-[10px] uppercase tracking-[0.35em] text-neon-cyan/70">spotlight</p>
<h2 className="font-orbitron text-3xl font-bold md:text-4xl">CURATED DROPS</h2>
<h2 className="font-orbitron text-3xl font-bold md:text-4xl">ARCANE ARTIFACTS</h2>
<p className="mt-3 text-foreground/60">
Hand-picked listings from <strong className="text-neon-cyan">{SHOP_PRODUCT_COUNT}</strong> in-stock SKUs ·{" "}
Conjured relics from <strong className="text-neon-cyan">{SHOP_PRODUCT_COUNT}</strong> in-stock tomes ·{" "}
<Link href="/vendors" className="text-neon-purple underline hover:text-neon-cyan">
{SHOP_SELLER_COUNT} vendor stalls
{SHOP_SELLER_COUNT} alchemist guilds
</Link>
. Verify PGP on the signed mirror list before your first order.
. Verify runic signatures on the astral mirror list before your first blood pact.
</p>
</div>
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
@@ -78,9 +78,9 @@ export default function Home() {
</section>
{/* Category Themes */}
<section id="categories" className="container mx-auto max-w-6xl px-4 py-20 scroll-mt-28">
<p className="mb-2 text-center font-mono text-[10px] uppercase tracking-[0.35em] text-neon-purple/70">browse</p>
<h2 className="mb-10 text-center font-orbitron text-3xl font-bold md:text-4xl">CATEGORY THEMES</h2>
<section id="categories" className="container mx-auto max-w-6xl px-4 py-20 scroll-mt-24">
<p className="mb-2 text-center font-mono text-[10px] uppercase tracking-[0.35em] text-neon-purple/70">summon</p>
<h2 className="mb-10 text-center font-orbitron text-3xl font-bold md:text-4xl">SCHOOLS OF MAGIC</h2>
<div className="grid grid-cols-2 gap-6 md:grid-cols-5">
{categories.map((cat) => (
<Link
@@ -102,12 +102,12 @@ export default function Home() {
<div className="glass overflow-hidden rounded-3xl border border-white/10 p-10 md:p-16">
<div className="grid items-center gap-10 md:grid-cols-2">
<div>
<h2 className="font-orbitron text-4xl font-bold">THE VAULT</h2>
<h2 className="font-orbitron text-4xl font-bold">THE GRIMOIRE</h2>
<p className="my-6 text-xl text-foreground/80">
A secure, encrypted space for returning customers. Store digital receipts, unlock loyalty rewards, and access exclusive content.
A secure, enchanted space for initiated warlocks. Store spectral receipts, unlock dark rewards, and access forbidden spells.
</p>
<Link href="/vault" className="rounded-full bg-gradient-to-r from-neon-green to-neon-cyan px-8 py-4 font-bold text-background inline-block">
UNLOCK YOUR VAULT
OPEN YOUR GRIMOIRE
</Link>
</div>
<div className="relative">
@@ -255,10 +255,10 @@ export default function Home() {
<div>
<div className="mb-4 flex items-center gap-3">
<div className="h-10 w-10 rounded-full bg-gradient-to-br from-neon-cyan to-neon-purple"></div>
<div className="font-orbitron text-2xl font-bold">CyberLux</div>
<div className="font-orbitron text-2xl font-bold">EldritchWeave</div>
</div>
<p className="text-foreground/60">
A firstofitskind cyberpunkluxury shopping experience.
A firstofitskind dark magic hacker gathering.
</p>
</div>
<div>
@@ -296,9 +296,9 @@ export default function Home() {
</div>
</div>
<div className="mt-12 border-t border-white/10 pt-8 text-center text-sm text-foreground/45">
<p>© 2026 CyberLux · PGP-signed mirrors · Escrow mandatory for new vendors</p>
<p>© 2026 EldritchWeave · Runic-signed mirrors · Blood pact mandatory for new summoners</p>
<p className="mt-2 text-foreground/35">
Finalize only after delivery. Report impostor onions with signed proof. Listings are user-generated verify before you pay.
Finalize only after delivery. Report impostor realms with signed proof. Spells are user-generated verify before you pay.
</p>
</div>
</div>

View File

@@ -1,7 +1,8 @@
"use client";
import { useState, useEffect, useCallback } from "react";
import { useState, useEffect, useCallback, useMemo } from "react";
import Link from "next/link";
import PayWithUsdBalanceButton from "@/components/PayWithUsdBalanceButton";
import ThemedLayout from "@/components/layouts/ThemedLayout";
import { getMerchantBtcAddress, isMerchantBtcConfigured } from "@/lib/merchantBtc";
import { useAccount } from "@/contexts/AccountContext";
@@ -58,9 +59,50 @@ export default function RafflePage() {
const [txid, setTxid] = useState("");
const [status, setStatus] = useState<"idle" | "success" | "error">("idle");
const [errMsg, setErrMsg] = useState("");
const [btcUsd, setBtcUsd] = useState<number | null>(null);
const btcAddr = getMerchantBtcAddress();
const btcReady = isMerchantBtcConfigured();
const ticketUsd = useMemo(() => {
const r = btcUsd ?? 96000;
const v = parseFloat(TICKET_PRICE_BTC) * r;
return Math.round(v * 100) / 100;
}, [btcUsd]);
useEffect(() => {
let cancelled = false;
void (async () => {
try {
const res = await fetch(
"https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd",
{ cache: "no-store" },
);
const j = (await res.json()) as { bitcoin?: { usd?: number } };
if (!cancelled && j.bitcoin?.usd && Number.isFinite(j.bitcoin.usd)) setBtcUsd(j.bitcoin.usd);
} catch {
if (!cancelled) setBtcUsd(96000);
}
})();
return () => {
cancelled = true;
};
}, []);
const registerBalanceTicket = useCallback(() => {
const t = `balance-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
const entry: Entry = {
handle: user?.username ?? "anon",
txid: t,
ts: new Date().toISOString(),
};
const cur = loadEntries();
const updated = [...cur, entry];
saveEntries(updated);
setEntries(updated);
setStatus("success");
setTimeout(() => setStatus("idle"), 4000);
}, [user?.username]);
useEffect(() => {
setEntries(loadEntries());
const tick = () => setRemaining(getNextDraw() - Date.now());
@@ -121,6 +163,7 @@ export default function RafflePage() {
<div className="border-2 border-white p-5 text-center">
<div className="mb-1 text-[10px] uppercase text-white/60">Ticket Price</div>
<div className="text-2xl font-black">{TICKET_PRICE_BTC} BTC</div>
<div className="mt-1 text-[11px] text-[#00ff41]/80"> ${ticketUsd.toFixed(2)} USD @ spot</div>
</div>
<div className="border-2 border-white p-5 text-center">
<div className="mb-1 text-[10px] uppercase text-white/60">Entries This Round</div>
@@ -182,6 +225,21 @@ export default function RafflePage() {
<p className="mt-3 text-sm text-red-400">{errMsg}</p>
)}
<div className="mt-6 border-2 border-[#00ff41]/40 bg-black/50 p-4">
<h4 className="mb-2 text-sm font-black uppercase text-[#00ff41]">Pay with USD balance</h4>
<p className="mb-3 text-[11px] text-white/60 leading-relaxed">
Same ticket charges your Bitcoin-funded USD balance at ${ticketUsd.toFixed(2)} (live BTC spot). No separate txid needed.
</p>
<PayWithUsdBalanceButton
amountUsd={ticketUsd}
title="Shadow Raffle ticket"
description="Weekly draw entry"
luxBonus={50}
onSuccess={registerBalanceTicket}
size="sm"
/>
</div>
{!user && (
<p className="mt-3 text-xs text-white/50">
<Link href="/sign-in?next=/raffle" className="text-[#00ff41] underline">Sign in</Link>{" "}

View File

@@ -8,26 +8,48 @@ import HubChatterPublicStrip from "@/components/HubChatterPublicStrip";
import AppProviders from "@/components/AppProviders";
import DarkWebLaunchFooter from "@/components/DarkWebLaunchFooter";
const DDOS_KEY = "ddos_verified";
function readDdosVerified(): boolean {
if (typeof window === "undefined") return false;
try {
if (sessionStorage.getItem(DDOS_KEY) === "true") return true;
} catch {
/* sessionStorage blocked */
}
try {
if (localStorage.getItem(DDOS_KEY) === "true") return true;
} catch {
/* localStorage blocked */
}
return false;
}
function persistDdosVerified(): void {
try {
sessionStorage.setItem(DDOS_KEY, "true");
} catch {
/* ignore */
}
try {
localStorage.setItem(DDOS_KEY, "true");
} catch {
/* ignore */
}
}
export default function RootLayoutClient({ children }: { children: React.ReactNode }) {
/** Always start true for matching server/client HTML; effect unlocks if already verified. */
const [isProtected, setIsProtected] = useState(true);
useEffect(() => {
try {
const hasSeenDDoS = sessionStorage.getItem("ddos_verified");
if (hasSeenDDoS) {
setIsProtected(false);
}
} catch {
// sessionStorage blocked (rare) — still allow progress overlay to complete
if (readDdosVerified()) {
setIsProtected(false);
}
}, []);
const handleComplete = useCallback(() => {
try {
sessionStorage.setItem("ddos_verified", "true");
} catch {
/* ignore */
}
persistDdosVerified();
setIsProtected(false);
}, []);

View File

@@ -2,152 +2,242 @@
import Link from "next/link";
import { useState, useEffect, useCallback } from "react";
import { useAccount } from "@/contexts/AccountContext";
export default function VaultNetworkPage() {
type Msg = { id: string; from: string; text: string; ts: number; channel?: string };
export default function VaultAdminDashboard() {
const { user, hydrated, signIn } = useAccount();
const [btcUsd, setBtcUsd] = useState<number | null>(null);
const [btcErr, setBtcErr] = useState<string | null>(null);
const [messages, setMessages] = useState<Msg[]>([]);
const [broadcast, setBroadcast] = useState("");
const [terminalLines, setTerminalLines] = useState<string[]>([]);
const [sessionToken, setSessionToken] = useState("");
const [loginPass, setLoginPass] = useState("");
const [loginError, setLoginError] = useState("");
const fetchBtc = useCallback(async () => {
try {
const res = await fetch("https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd", {
cache: "no-store",
});
if (!res.ok) throw new Error("rate http");
const j = (await res.json()) as { bitcoin?: { usd?: number } };
const p = j.bitcoin?.usd;
if (p && Number.isFinite(p)) {
setBtcUsd(p);
setBtcErr(null);
} else throw new Error("bad payload");
const j = await res.json();
setBtcUsd(j.bitcoin?.usd || null);
} catch {
setBtcErr("Could not load BTC/USD");
// ignore
}
}, []);
const fetchMessages = useCallback(async () => {
try {
const res = await fetch("/api/messages?channel=all");
const data = await res.json();
if (data.ok) setMessages(data.messages);
} catch {
// ignore
}
}, []);
useEffect(() => {
setSessionToken(
typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID().slice(0, 13) : String(Date.now()),
);
}, []);
useEffect(() => {
void fetchBtc();
const id = setInterval(() => void fetchBtc(), 60_000);
void fetchMessages();
const id = setInterval(() => {
void fetchBtc();
void fetchMessages();
}, 10_000);
return () => clearInterval(id);
}, [fetchBtc]);
}, [fetchBtc, fetchMessages]);
useEffect(() => {
const interval = setInterval(() => {
const logs = [
`[TOR] circuit refresh (synthetic tick — not your live Tor log)`,
`[NGINX] 200 GET /market from 127.0.0.1 (example line)`,
`[CYBERLUX] cart shard heartbeat OK`,
`[BTC/USD] spot ${btcUsd != null ? `$${btcUsd.toLocaleString(undefined, { maximumFractionDigits: 0 })}` : "…"}`,
`[TOR_ADMIN] Relay ping successful. Circuit latency: ${Math.floor(Math.random() * 80 + 20)}ms`,
`[NGINX_ROUTER] Routing loopback 127.0.0.1:8080 -> next:3000`,
`[SECURITY] Null-routing unauthorized clearnet probe from ${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}`,
`[BTC_NODE] Synced block height ${839000 + Math.floor(Math.random() * 1000)}`,
`[SYSTEM] Memory stable. Cache flushed.`,
];
setTerminalLines((prev) => [...prev.slice(-15), logs[Math.floor(Math.random() * logs.length)]!]);
}, 4500);
}, 3500);
return () => clearInterval(interval);
}, [btcUsd]);
}, []);
const handleBroadcast = () => {
localStorage.setItem("shadow_broadcast", broadcast);
setTerminalLines((prev) => [...prev, `[ADMIN] GLOBAL BROADCAST: ${broadcast || "(empty)"}`]);
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setLoginError("");
const res = await signIn("drjones", loginPass);
if (!res.ok) {
setLoginError(res.error || "Access Denied");
}
};
const btcDisplay =
btcUsd != null
? btcUsd.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })
: btcErr ?? "…";
const handleBroadcast = async () => {
if (!broadcast.trim()) return;
try {
await fetch("/api/messages", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ from: "System Admin", text: `[GLOBAL BROADCAST] ${broadcast}`, channel: "global" }),
});
setTerminalLines((prev) => [...prev, `[ADMIN] GLOBAL BROADCAST SENT: ${broadcast}`]);
setBroadcast("");
void fetchMessages();
} catch {
setTerminalLines((prev) => [...prev, `[ADMIN] Broadcast Failed!`]);
}
};
const deleteMessage = async (id: string) => {
await fetch(`/api/messages?id=${id}`, { method: "DELETE" });
void fetchMessages();
setTerminalLines((prev) => [...prev, `[ADMIN] Deleted message ${id}`]);
};
const clearAllMessages = async () => {
if (!confirm("Are you sure you want to nuke all global communications?")) return;
await fetch(`/api/messages?all=true`, { method: "DELETE" });
void fetchMessages();
setTerminalLines((prev) => [...prev, `[ADMIN] Nuked all global messages.`]);
};
if (!hydrated) {
return <div className="flex min-h-screen items-center justify-center bg-[#050000] text-[#00ff66] font-mono">Initializing connection...</div>;
}
// Only drjones gets to see the true dashboard
if (!user || user.username !== "drjones") {
return (
<div className="flex min-h-screen flex-col items-center justify-center bg-[#050000] p-6 font-mono text-[#00ff66]">
<div className="w-full max-w-md border border-[#00ff66]/30 bg-black/80 p-8 shadow-[0_0_30px_rgba(0,255,102,0.15)] rounded-lg text-center">
<h1 className="mb-2 text-2xl font-black uppercase text-red-500 tracking-widest">RESTRICTED ZONE</h1>
<p className="mb-8 text-xs text-foreground/60">EldritchWeave Tor Network Admin</p>
<form onSubmit={handleLogin} className="flex flex-col gap-4 text-left">
<div>
<label className="text-xs uppercase text-[#00ff66]/70">Admin Passphrase</label>
<input
type="password"
value={loginPass}
onChange={(e) => setLoginPass(e.target.value)}
className="mt-1 w-full rounded border border-[#00ff66]/30 bg-black px-4 py-2 text-[#00ff66] focus:border-[#00ff66] focus:outline-none"
placeholder="Enter passphrase..."
/>
</div>
{loginError && <div className="text-xs font-bold text-red-500">{loginError}</div>}
<button type="submit" className="mt-2 w-full bg-[#00ff66]/20 py-3 font-bold uppercase text-[#00ff66] hover:bg-[#00ff66]/30 transition-colors">
AUTHORIZE
</button>
</form>
<Link href="/" className="mt-8 block text-[10px] uppercase text-[#00ff66]/40 hover:text-[#00ff66] underline">
Return to Hub
</Link>
</div>
</div>
);
}
return (
<div className="flex min-h-screen flex-col overflow-hidden bg-[#050000] p-6 font-mono text-[#00ff41]">
<header className="mb-6 flex flex-wrap items-center justify-between gap-4 border-b border-[#00ff41]/30 pb-4">
<div className="flex min-h-screen flex-col overflow-hidden bg-[#030008] p-6 font-mono text-[#00ff66]">
<header className="mb-6 flex flex-wrap items-center justify-between gap-4 border-b border-[#00ff66]/30 pb-4">
<div className="flex items-center gap-4">
<div className="h-3 w-3 animate-ping rounded-full bg-red-600" />
<h1 className="text-xl font-black tracking-tighter text-white uppercase">Network digest</h1>
<div className="h-3 w-3 animate-ping rounded-full bg-red-600 shadow-[0_0_10px_red]" />
<h1 className="text-2xl font-black tracking-tighter text-white uppercase text-shadow-sm shadow-red-500">Tor Admin Dashboard</h1>
</div>
<div className="flex flex-wrap gap-6 text-[10px] uppercase tracking-widest opacity-80">
<div>UI session: {sessionToken || "—"}</div>
<div className="text-green-500">Not live Tor admin decorative console</div>
<div>Admin: <span className="text-red-400">Dr. Jones</span></div>
<div>Status: <span className="text-[#00ff66]">GOD MODE</span></div>
</div>
</header>
<div className="grid flex-1 grid-cols-12 gap-6">
<div className="col-span-12 space-y-6 lg:col-span-4">
<div className="border border-[#00ff41]/20 bg-black p-6 shadow-[0_0_20px_rgba(0,255,65,0.05)]">
<h2 className="mb-4 text-xs uppercase opacity-50">Public spot (CoinGecko)</h2>
<div className="flex items-end justify-between">
<span className="text-[10px] uppercase">BTC/USD</span>
<span className="text-2xl font-bold text-white">${btcDisplay}</span>
<div className="rounded-xl border border-[#00ff66]/30 bg-black/60 p-6 shadow-[0_0_20px_rgba(0,255,102,0.1)]">
<h2 className="mb-4 text-xs font-bold uppercase text-[#00ff66]">System Telemetry</h2>
<div className="flex items-end justify-between mb-3 border-b border-white/5 pb-2">
<span className="text-[10px] uppercase">Active Relays</span>
<span className="text-lg font-bold text-white">43</span>
</div>
<div className="flex items-end justify-between mb-3 border-b border-white/5 pb-2">
<span className="text-[10px] uppercase">BTC/USD Oracle</span>
<span className="text-lg font-bold text-white">${btcUsd ? btcUsd.toLocaleString() : "..."}</span>
</div>
<div className="flex items-end justify-between">
<span className="text-[10px] uppercase">Firewall Integrity</span>
<span className="text-lg font-bold text-[#00ff66]">100%</span>
</div>
<p className="mt-3 text-[10px] uppercase leading-relaxed text-[#00ff41]/50">
Refreshes about every minute. For deposits use{" "}
<Link href="/account/add-funds" className="text-[#7dd3fc] underline">
Add funds
</Link>
.
</p>
</div>
<div className="border border-red-600/40 bg-black p-6 shadow-[0_0_20px_rgba(255,0,0,0.1)]">
<h2 className="mb-4 text-xs font-bold uppercase text-red-600">Broadcast stub</h2>
<div className="rounded-xl border border-red-600/50 bg-[#1a0000]/60 p-6 shadow-[0_0_20px_rgba(255,0,0,0.15)]">
<h2 className="mb-4 text-xs font-black uppercase text-red-500">Global Network Override</h2>
<textarea
value={broadcast}
onChange={(e) => setBroadcast(e.target.value)}
className="mb-4 h-24 w-full resize-none border border-red-900 bg-[#111] p-3 text-xs text-red-500 focus:outline-none"
placeholder="Message stored in localStorage key shadow_broadcast…"
className="mb-4 h-24 w-full resize-none rounded border border-red-900/50 bg-[#0a0000] p-3 text-xs text-red-400 focus:border-red-500 focus:outline-none"
placeholder="Force a message to all users on the network..."
/>
<button
type="button"
onClick={handleBroadcast}
className="w-full bg-red-900/90 py-2 text-xs font-bold uppercase text-red-100 transition-all hover:bg-red-800"
className="w-full rounded bg-red-900/80 py-3 text-xs font-bold uppercase text-white transition-all hover:bg-red-700 shadow-[0_0_10px_rgba(255,0,0,0.3)]"
>
Store broadcast (local)
EXECUTE OVERRIDE
</button>
</div>
<div className="relative h-48 overflow-hidden border border-[#00ff41]/20 bg-black p-6">
<h2 className="mb-4 text-xs uppercase opacity-50">Node map</h2>
<p className="relative z-10 text-[8px] leading-relaxed opacity-80">
Decorative. Real topology is Tor + nginx on your host see README / DEPLOY.md.
</p>
<div className="rounded-xl border border-[#00ff66]/20 bg-black p-6">
<h2 className="mb-4 text-xs uppercase text-[#00ff66]">Network Operations Logs</h2>
<div className="flex h-48 flex-col overflow-hidden rounded bg-[#030005] p-3 text-[9px] text-[#00ff66]/70 border border-white/5">
<div className="scrollbar-hide flex-1 space-y-1 overflow-y-auto font-mono">
{terminalLines.map((line, i) => (
<div key={i} className={line.includes("[ADMIN]") ? "text-red-400 font-bold" : line.includes("SECURITY") ? "text-yellow-400" : ""}>
{line}
</div>
))}
<div className="animate-pulse">_</div>
</div>
</div>
</div>
</div>
<div className="col-span-12 flex flex-col gap-6 lg:col-span-8">
<div className="flex min-h-[200px] flex-1 flex-col overflow-hidden border border-[#00ff41]/20 bg-black p-4 text-[10px]">
<div className="mb-2 flex items-center justify-between border-b border-[#00ff41]/10 pb-2">
<span className="uppercase opacity-50">Chatter console</span>
<span className="animate-pulse text-green-500"> FEED</span>
<div className="flex flex-1 flex-col overflow-hidden rounded-xl border border-[#00ff66]/30 bg-black/60 p-6">
<div className="mb-4 flex items-center justify-between border-b border-[#00ff66]/20 pb-4">
<span className="font-bold uppercase text-[#00ff66] tracking-wider text-sm">Comms Interception (Wiretap)</span>
<button
onClick={clearAllMessages}
className="rounded border border-red-900/50 bg-red-900/20 px-4 py-1 text-[10px] font-bold text-red-500 hover:bg-red-900/40"
>
NUKE ALL
</button>
</div>
<div className="scrollbar-hide flex-1 space-y-1 overflow-y-auto">
{terminalLines.map((line, i) => (
<div key={`${i}-${line.slice(0, 24)}`} className={line.includes("[ADMIN]") ? "font-bold text-red-500" : ""}>
{line}
</div>
))}
<div className="animate-pulse">_</div>
</div>
</div>
<div className="border border-[#00ff41]/20 bg-black/80 p-4 text-[10px] uppercase tracking-widest opacity-50">
<Link href="/dashboard" className="text-[#7dd3fc] hover:underline">
Dashboard
</Link>
{" · "}
<Link href="/checkout" className="text-[#7dd3fc] hover:underline">
Checkout
</Link>
{" · "}
<Link href="/" className="text-[#7dd3fc] hover:underline">
Hub
</Link>
<div className="scrollbar-hide flex-1 space-y-3 overflow-y-auto pr-2">
{messages.length === 0 ? (
<div className="flex h-full items-center justify-center text-xs opacity-40">No network traffic detected...</div>
) : (
messages.map((msg) => (
<div key={msg.id} className="flex flex-col gap-1 rounded bg-[#0a110d] p-3 border border-[#00ff66]/10">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-[11px] font-bold text-[#00ff66]">@{msg.from}</span>
<span className="text-[9px] opacity-50 uppercase bg-black px-1.5 py-0.5 rounded">CH: {msg.channel || "global"}</span>
<span className="text-[9px] opacity-40">{new Date(msg.ts).toLocaleString()}</span>
</div>
<button
onClick={() => deleteMessage(msg.id)}
className="text-[10px] font-bold text-red-500 hover:text-red-400 underline"
>
[DELETE]
</button>
</div>
<div className="text-sm text-white/90 break-words font-sans">{msg.text}</div>
</div>
)).reverse()
)}
</div>
</div>
</div>
</div>
</div>
);
}
}

View File

@@ -2,6 +2,7 @@
import { useState } from "react";
import Link from "next/link";
import PayWithUsdBalanceButton from "@/components/PayWithUsdBalanceButton";
import ThemedLayout from "@/components/layouts/ThemedLayout";
import { useCart } from "@/contexts/CartContext";
import type { ShopProduct } from "@/lib/shopCatalog";
@@ -164,6 +165,17 @@ export default function DigitalPassesPage() {
>
{isAdded ? "✓ Added to Cart" : `Add to Cart — $${pass.price}`}
</button>
<div className="mt-3 border border-[#0f0]/25 bg-black/60 p-3">
<p className="mb-2 text-[10px] font-bold uppercase text-[#0f0]/70">Or pay instantly (BTC-funded USD)</p>
<PayWithUsdBalanceButton
amountUsd={pass.price}
title={`Pass: ${pass.name}`}
description={`Tier ${pass.tier} · +${pass.luxReward} LUX`}
luxBonus={pass.luxReward}
size="sm"
className="w-full border-[#0f0]/40 !text-[#0f0]"
/>
</div>
</div>
);
})}