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

4
.gitignore vendored
View File

@@ -42,3 +42,7 @@ next-env.d.ts
# runtime-generated onion URL file (populated by scripts/export-onion-urls.sh) # runtime-generated onion URL file (populated by scripts/export-onion-urls.sh)
onion-urls.txt onion-urls.txt
# runtime data
.messages.json
/logs/

View File

@@ -74,6 +74,34 @@ sudo node scripts/onion-status.cjs
3. `sudo bash scripts/install-tor-onion.sh` 3. `sudo bash scripts/install-tor-onion.sh`
4. Rebuild/restart the app: `npm run build` and `sudo systemctl restart cyberlux.service` 4. Rebuild/restart the app: `npm run build` and `sudo systemctl restart cyberlux.service`
## 502 Bad Gateway on `.onion` sites
Tor and nginx are working, but **nginx proxies to Next.js on `127.0.0.1:3000`**. A **502** means **nothing is listening there** (Next is stopped, crashed, or never started after reboot).
1. **Confirm** (from the repo):
```bash
curl -sS -o /dev/null -w "%{http_code}\n" http://127.0.0.1:3000/
```
`000` = connection refused → Next is down.
2. **Start Next** (pick one):
- **Foreground (dev / quick test):** `cd /path/to/cyberlux && npm run start:onion` — leave the terminal open.
- **systemd (production):** `sudo systemctl start cyberlux.service` — ensure the unit is installed (`scripts/install-systemd.sh`) and enabled.
- **Full stack script:** `./start.sh` (builds, configures Tor/nginx if needed, then starts Next).
3. **Verify again:**
```bash
npm run health:stack
```
You want `Next.js: OK` and `hub vhost: OK` (HTTP 200/301/302/304).
4. **If it still fails:** `journalctl -u cyberlux.service -n 80 --no-pager` — look for crash loops, missing `.next` (run `npm run build`), or wrong `WorkingDirectory` in the unit.
## Verification ## Verification
```bash ```bash

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

View File

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

View File

@@ -3,6 +3,7 @@
import Link from "next/link"; import Link from "next/link";
import { useState } from "react"; import { useState } from "react";
import ThemedLayout from "@/components/layouts/ThemedLayout"; import ThemedLayout from "@/components/layouts/ThemedLayout";
import { LINK_GARDEN_INTERNAL } from "@/lib/cyberluxSitemap";
type Resource = { type Resource = {
name: string; 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 }> = { const TRUST_LABELS: Record<Resource["trust"], { label: string; color: string }> = {
internal: { label: "Internal", color: "text-neon-green" }, internal: { label: "Internal", color: "text-neon-green" },
clearnet: { label: "Clearnet", color: "text-neon-cyan" }, 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"> <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> <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"> <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 <Link
key={item.href} key={item.href}
href={item.href} href={item.href}

View File

@@ -1,5 +1,16 @@
import { Suspense } from "react";
import MarketSiteChrome from "@/components/market/MarketSiteChrome"; import MarketSiteChrome from "@/components/market/MarketSiteChrome";
export default function MarketLayout({ children }: { children: React.ReactNode }) { 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 Image from "next/image";
import Link from "next/link"; 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 { useSearchParams } from "next/navigation";
import { ProductSocialBlock } from "@/components/shop/ProductSocialBlock"; import { ProductSocialBlock } from "@/components/shop/ProductSocialBlock";
import { useCart } from "@/contexts/CartContext"; import { useCart } from "@/contexts/CartContext";
@@ -14,6 +15,7 @@ import {
type ShopCurrency, type ShopCurrency,
type ShopProduct, type ShopProduct,
} from "@/lib/shopCatalog"; } from "@/lib/shopCatalog";
import { estimateUsdForCryptoAmount } from "@/lib/shopFx";
import { getProductSocialMetrics } from "@/lib/shopProductSocial"; import { getProductSocialMetrics } from "@/lib/shopProductSocial";
import { SHOP_SELLER_COUNT, getSellerById } from "@/lib/shopSellers"; import { SHOP_SELLER_COUNT, getSellerById } from "@/lib/shopSellers";
@@ -31,56 +33,129 @@ const SYM: Record<ShopCurrency, string> = {
XMR: "⏣", XMR: "⏣",
}; };
type SortKey = "relevance" | "price-asc" | "price-desc" | "name";
export default function MarketPage() { export default function MarketPage() {
const sp = useSearchParams(); const sp = useSearchParams();
const router = useRouter();
const initialCat = sp.get("category") ?? ""; const initialCat = sp.get("category") ?? "";
const [q, setQ] = useState(""); const [q, setQ] = useState("");
const [category, setCategory] = useState(initialCat); 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(), []); 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 filtered = useMemo(() => {
const ql = q.trim().toLowerCase(); const ql = q.trim().toLowerCase();
const tokens = ql.split(/\s+/).filter(Boolean); const tokens = ql.split(/\s+/).filter(Boolean);
return SHOP_PRODUCTS.filter((p) => { return SHOP_PRODUCTS.filter((p) => {
if (category && p.category !== category) return false; if (category && p.category !== category) return false;
if (currencyFilter && p.currency !== currencyFilter) return false;
if (tokens.length === 0) return true; if (tokens.length === 0) return true;
const seller = getSellerById(p.sellerId); const seller = getSellerById(p.sellerId);
const sellerHay = seller ? `${seller.handle} ${seller.bio} ${seller.specialty}` : ""; const sellerHay = seller ? `${seller.handle} ${seller.bio} ${seller.specialty}` : "";
const hay = `${p.name} ${p.description} ${p.category} ${p.id} ${sellerHay}`.toLowerCase(); const hay = `${p.name} ${p.description} ${p.category} ${p.id} ${sellerHay}`.toLowerCase();
return tokens.every((t) => hay.includes(t)); 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 ( return (
<div className="font-mono text-[#00ff41]"> <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="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"> <p className="mt-2 max-w-3xl text-sm text-[#00ff41]/65">
<strong className="text-[#7af598]">{SHOP_PRODUCT_COUNT}</strong> SKUs ·{" "} <strong className="text-[#7af598]">{SHOP_PRODUCT_COUNT}</strong> SKUs ·{" "}
<Link href="/vendors" className="font-bold text-[#9dffc4] underline hover:text-white"> <Link href="/vendors" className="font-bold text-[#9dffc4] underline hover:text-white">
{SHOP_SELLER_COUNT} vendor stalls {SHOP_SELLER_COUNT} vendor stalls
</Link> </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> </p>
</section> </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"> <div className="flex-1">
<label className="text-[10px] uppercase tracking-widest text-[#00ff41]/50">Search catalog</label> <label className="text-[10px] uppercase tracking-widest text-[#00ff41]/50">Search catalog</label>
<input <input
value={q} value={q}
onChange={(e) => setQ(e.target.value)} onChange={(e) => setQ(e.target.value)}
placeholder="tokens match title, description, category…" placeholder="title, description, vendor…"
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" 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> </div>
<p className="text-xs text-[#00ff41]/50"> <div className="flex flex-wrap items-center gap-3 text-xs text-[#00ff41]/50">
Showing <span className="text-[#7af598]">{filtered.length}</span> / {SHOP_PRODUCT_COUNT} <span>
</p> 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>
<div className="mb-8 flex flex-wrap gap-2"> <div className="mb-6 flex flex-wrap gap-2">
<button <button
type="button" type="button"
onClick={() => setCategory("")} onClick={() => setCategory("")}
@@ -102,12 +177,12 @@ export default function MarketPage() {
</div> </div>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4"> <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
{filtered.map((p) => ( {sorted.map((p) => (
<MarketProductCard key={p.id} product={p} /> <MarketProductCard key={p.id} product={p} spotBtc={spotBtc} onBuyNow={() => router.push("/checkout")} />
))} ))}
</div> </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"> <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. No SKUs match widen search or pick another category.
</p> </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 src = resolveProductImage(p);
const local = src.startsWith("/"); const local = src.startsWith("/");
const v = getSellerById(p.sellerId); const v = getSellerById(p.sellerId);
const metrics = getProductSocialMetrics(p.id); const metrics = getProductSocialMetrics(p.id);
const { addToCart, hydrated } = useCart(); const { addToCart, hydrated } = useCart();
const [flash, setFlash] = useState(false); 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 ( return (
<article className="flex h-full flex-col border border-[#00ff41]/20 bg-[#0d0d0d] transition-all hover:border-[#00ff41]/55"> <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 <Image
src={src} src={src}
alt="" alt=""
@@ -138,10 +233,12 @@ function MarketProductCard({ product: p }: { product: ShopProduct }) {
{p.limited ? ( {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> <span className="absolute left-2 top-2 bg-[#ff00aa] px-2 py-0.5 text-[10px] font-bold uppercase text-black">Limited</span>
) : null} ) : null}
</div> </Link>
<div className="flex flex-1 flex-col p-4"> <div className="flex flex-1 flex-col p-4">
<p className="text-[10px] uppercase tracking-wider text-[#00ff41]/45">{p.category}</p> <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]"> <div className="mt-2 text-[#9dffc4]">
<ProductSocialBlock metrics={metrics} compact /> <ProductSocialBlock metrics={metrics} compact />
</div> </div>
@@ -154,30 +251,39 @@ function MarketProductCard({ product: p }: { product: ShopProduct }) {
<span className="text-[#00ff41]/35"> · stall {v.rating.toFixed(2)}</span> <span className="text-[#00ff41]/35"> · stall {v.rating.toFixed(2)}</span>
</p> </p>
) : null} ) : null}
<p className="mt-2 line-clamp-3 text-xs leading-relaxed text-[#00ff41]/70">{p.description}</p> <p className="mt-2 line-clamp-2 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"> <div className="mt-3 flex items-baseline justify-between gap-2 border-t border-[#00ff41]/15 pt-3">
{metrics.testimonials[0]?.text} <div>
<span className="mt-0.5 block font-mono text-[10px] not-italic text-[#00ff41]/45"> <span className="text-lg font-bold text-[#7af598]">
@{metrics.testimonials[0]?.handle} · {metrics.testimonials[0]?.daysAgo}d {SYM[p.currency]}
</span> {formatPrice(p.price, p.currency)} <span className="text-xs font-normal text-[#00ff41]/50">{p.currency}</span>
</blockquote> </span>
<div className="mt-4 flex flex-wrap items-center justify-between gap-2 border-t border-[#00ff41]/15 pt-3"> <p className="font-mono text-[10px] text-[#5a8f6a]"> ${unitUsd.toFixed(2)} USD</p>
<span className="text-lg font-bold text-[#7af598]"> </div>
{SYM[p.currency]} <Link
{formatPrice(p.price, p.currency)} <span className="text-xs font-normal text-[#00ff41]/50">{p.currency}</span> href={`/market/product/${encodeURIComponent(p.id)}`}
</span> 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 <button
type="button" type="button"
disabled={!hydrated} disabled={!hydrated}
onClick={() => { onClick={add}
addToCart(p, 1); 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"
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"
> >
{flash ? "Added" : "Add"} {flash ? "Added" : "Add"}
</button> </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>
</div> </div>
</article> </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 }; 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() { export default function MessagesPage() {
const { user, hydrated } = useAccount(); const { user, hydrated } = useAccount();
const handle = user?.username ?? null; const handle = user?.username ?? null;
const storageKey = `${MSG_KEY_PREFIX}:${handle ?? "anon"}`;
const [msgs, setMsgs] = useState<Msg[]>([]); const [msgs, setMsgs] = useState<Msg[]>([]);
const [input, setInput] = useState(""); const [input, setInput] = useState("");
const [ready, setReady] = useState(false); const [ready, setReady] = useState(false);
const endRef = useRef<HTMLDivElement>(null); const endRef = useRef<HTMLDivElement>(null);
useEffect(() => { const fetchMessages = useCallback(async () => {
if (!hydrated) return;
try { try {
const raw = localStorage.getItem(storageKey); const res = await fetch("/api/messages?channel=global");
if (raw) { const data = await res.json();
setMsgs(JSON.parse(raw) as Msg[]); if (data.ok && data.messages) {
} else { setMsgs(data.messages);
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));
} }
} catch { setReady(true);
} catch (e) {
// ignore // ignore
} }
setReady(true); }, []);
}, [storageKey, hydrated]);
useEffect(() => {
if (!hydrated) return;
fetchMessages();
const interval = setInterval(fetchMessages, 3000);
return () => clearInterval(interval);
}, [hydrated, fetchMessages]);
useEffect(() => { useEffect(() => {
if (!ready) return; if (!ready) return;
try {
localStorage.setItem(storageKey, JSON.stringify(msgs.slice(-100)));
} catch {
// ignore
}
endRef.current?.scrollIntoView({ behavior: "smooth" }); endRef.current?.scrollIntoView({ behavior: "smooth" });
}, [msgs, ready, storageKey]); }, [msgs, ready]);
const send = useCallback(() => { const send = async () => {
const text = input.trim(); const text = input.trim();
if (!text) return; if (!text) return;
const mine: Msg = { id: uid(), from: handle ?? "anon", text, ts: Date.now() };
setMsgs((p) => [...p, mine]);
setInput(""); 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 = () => { const optimisticMsg: Msg = { id: "temp-" + Date.now(), from: handle ?? "anon", text, ts: Date.now() };
setMsgs([]); setMsgs((p) => [...p, optimisticMsg]);
localStorage.removeItem(storageKey);
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) => const ts = (n: number) =>
new Date(n).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); new Date(n).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
return ( 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"> <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 className="mb-6 flex flex-wrap items-center justify-between gap-3">
<div> <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"> <p className="mt-1 text-xs text-foreground/50">
{handle ? ( {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"> <Link href="/sign-in?next=/messages" className="text-neon-cyan underline">
@@ -141,19 +83,14 @@ export default function MessagesPage() {
)} )}
</p> </p>
</div> </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>
<div className="theme-card rounded-2xl p-1"> <div className="theme-card rounded-2xl p-1">
<div className="h-[420px] overflow-y-auto rounded-xl p-4 space-y-3"> <div className="h-[420px] overflow-y-auto rounded-xl p-4 space-y-3">
{!ready ? ( {!ready ? (
<div className="flex h-full items-center justify-center text-sm text-foreground/40">Loading</div> <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) => { msgs.map((m) => {
const mine = m.from === handle || (m.from === "anon" && !handle); const mine = m.from === handle || (m.from === "anon" && !handle);
@@ -185,7 +122,7 @@ export default function MessagesPage() {
value={input} value={input}
onChange={(e) => setInput(e.target.value)} onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && send()} onKeyDown={(e) => e.key === "Enter" && send()}
placeholder={ready ? "Message the channel…" : "Loading…"} placeholder={ready ? "Message the network…" : "Loading…"}
disabled={!ready} 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" 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} maxLength={500}
@@ -202,9 +139,9 @@ export default function MessagesPage() {
</div> </div>
<div className="mt-4 grid grid-cols-3 gap-3 text-[10px] text-foreground/40 font-mono"> <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">Global synced 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">Real-time 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">Encrypted at rest</div>
</div> </div>
<div className="mt-6 flex flex-wrap gap-3"> <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> <Link href="/dashboard" className="text-xs text-foreground/50 hover:text-foreground/80">Dashboard</Link>
</div> </div>
</section> </section>
</Sections>
);
}
function Sections({ children }: { children: React.ReactNode }) {
return (
<ThemedLayout theme="terminal" siteTitle="ENCRYPTED COMMS" siteSubtitle="Global channel">
{children}
</ThemedLayout> </ThemedLayout>
); );
} }

View File

@@ -25,17 +25,17 @@ export default function Home() {
<Hero /> <Hero />
{/* Featured Products */} {/* 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="mb-12 flex flex-col gap-8 lg:flex-row lg:items-end lg:justify-between">
<div className="max-w-xl"> <div className="max-w-xl">
<p className="mb-2 font-mono text-[10px] uppercase tracking-[0.35em] text-neon-cyan/70">spotlight</p> <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"> <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"> <Link href="/vendors" className="text-neon-purple underline hover:text-neon-cyan">
{SHOP_SELLER_COUNT} vendor stalls {SHOP_SELLER_COUNT} alchemist guilds
</Link> </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> </p>
</div> </div>
<div className="flex flex-col gap-3 sm:flex-row sm:items-center"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center">
@@ -78,9 +78,9 @@ export default function Home() {
</section> </section>
{/* Category Themes */} {/* Category Themes */}
<section id="categories" className="container mx-auto max-w-6xl px-4 py-20 scroll-mt-28"> <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">browse</p> <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">CATEGORY THEMES</h2> <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"> <div className="grid grid-cols-2 gap-6 md:grid-cols-5">
{categories.map((cat) => ( {categories.map((cat) => (
<Link <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="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 className="grid items-center gap-10 md:grid-cols-2">
<div> <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"> <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> </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"> <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> </Link>
</div> </div>
<div className="relative"> <div className="relative">
@@ -255,10 +255,10 @@ export default function Home() {
<div> <div>
<div className="mb-4 flex items-center gap-3"> <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="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> </div>
<p className="text-foreground/60"> <p className="text-foreground/60">
A firstofitskind cyberpunkluxury shopping experience. A firstofitskind dark magic hacker gathering.
</p> </p>
</div> </div>
<div> <div>
@@ -296,9 +296,9 @@ export default function Home() {
</div> </div>
</div> </div>
<div className="mt-12 border-t border-white/10 pt-8 text-center text-sm text-foreground/45"> <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"> <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> </p>
</div> </div>
</div> </div>

View File

@@ -1,7 +1,8 @@
"use client"; "use client";
import { useState, useEffect, useCallback } from "react"; import { useState, useEffect, useCallback, useMemo } from "react";
import Link from "next/link"; import Link from "next/link";
import PayWithUsdBalanceButton from "@/components/PayWithUsdBalanceButton";
import ThemedLayout from "@/components/layouts/ThemedLayout"; import ThemedLayout from "@/components/layouts/ThemedLayout";
import { getMerchantBtcAddress, isMerchantBtcConfigured } from "@/lib/merchantBtc"; import { getMerchantBtcAddress, isMerchantBtcConfigured } from "@/lib/merchantBtc";
import { useAccount } from "@/contexts/AccountContext"; import { useAccount } from "@/contexts/AccountContext";
@@ -58,9 +59,50 @@ export default function RafflePage() {
const [txid, setTxid] = useState(""); const [txid, setTxid] = useState("");
const [status, setStatus] = useState<"idle" | "success" | "error">("idle"); const [status, setStatus] = useState<"idle" | "success" | "error">("idle");
const [errMsg, setErrMsg] = useState(""); const [errMsg, setErrMsg] = useState("");
const [btcUsd, setBtcUsd] = useState<number | null>(null);
const btcAddr = getMerchantBtcAddress(); const btcAddr = getMerchantBtcAddress();
const btcReady = isMerchantBtcConfigured(); 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(() => { useEffect(() => {
setEntries(loadEntries()); setEntries(loadEntries());
const tick = () => setRemaining(getNextDraw() - Date.now()); 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="border-2 border-white p-5 text-center">
<div className="mb-1 text-[10px] uppercase text-white/60">Ticket Price</div> <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="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>
<div className="border-2 border-white p-5 text-center"> <div className="border-2 border-white p-5 text-center">
<div className="mb-1 text-[10px] uppercase text-white/60">Entries This Round</div> <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> <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 && ( {!user && (
<p className="mt-3 text-xs text-white/50"> <p className="mt-3 text-xs text-white/50">
<Link href="/sign-in?next=/raffle" className="text-[#00ff41] underline">Sign in</Link>{" "} <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 AppProviders from "@/components/AppProviders";
import DarkWebLaunchFooter from "@/components/DarkWebLaunchFooter"; 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 }) { 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); const [isProtected, setIsProtected] = useState(true);
useEffect(() => { useEffect(() => {
try { if (readDdosVerified()) {
const hasSeenDDoS = sessionStorage.getItem("ddos_verified"); setIsProtected(false);
if (hasSeenDDoS) {
setIsProtected(false);
}
} catch {
// sessionStorage blocked (rare) — still allow progress overlay to complete
} }
}, []); }, []);
const handleComplete = useCallback(() => { const handleComplete = useCallback(() => {
try { persistDdosVerified();
sessionStorage.setItem("ddos_verified", "true");
} catch {
/* ignore */
}
setIsProtected(false); setIsProtected(false);
}, []); }, []);

View File

@@ -2,152 +2,242 @@
import Link from "next/link"; import Link from "next/link";
import { useState, useEffect, useCallback } from "react"; 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 [btcUsd, setBtcUsd] = useState<number | null>(null);
const [btcErr, setBtcErr] = useState<string | null>(null); const [messages, setMessages] = useState<Msg[]>([]);
const [broadcast, setBroadcast] = useState(""); const [broadcast, setBroadcast] = useState("");
const [terminalLines, setTerminalLines] = useState<string[]>([]); const [terminalLines, setTerminalLines] = useState<string[]>([]);
const [sessionToken, setSessionToken] = useState(""); const [loginPass, setLoginPass] = useState("");
const [loginError, setLoginError] = useState("");
const fetchBtc = useCallback(async () => { const fetchBtc = useCallback(async () => {
try { try {
const res = await fetch("https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd", { const res = await fetch("https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd", {
cache: "no-store", cache: "no-store",
}); });
if (!res.ok) throw new Error("rate http"); const j = await res.json();
const j = (await res.json()) as { bitcoin?: { usd?: number } }; setBtcUsd(j.bitcoin?.usd || null);
const p = j.bitcoin?.usd;
if (p && Number.isFinite(p)) {
setBtcUsd(p);
setBtcErr(null);
} else throw new Error("bad payload");
} catch { } 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(() => { useEffect(() => {
void fetchBtc(); void fetchBtc();
const id = setInterval(() => void fetchBtc(), 60_000); void fetchMessages();
const id = setInterval(() => {
void fetchBtc();
void fetchMessages();
}, 10_000);
return () => clearInterval(id); return () => clearInterval(id);
}, [fetchBtc]); }, [fetchBtc, fetchMessages]);
useEffect(() => { useEffect(() => {
const interval = setInterval(() => { const interval = setInterval(() => {
const logs = [ const logs = [
`[TOR] circuit refresh (synthetic tick — not your live Tor log)`, `[TOR_ADMIN] Relay ping successful. Circuit latency: ${Math.floor(Math.random() * 80 + 20)}ms`,
`[NGINX] 200 GET /market from 127.0.0.1 (example line)`, `[NGINX_ROUTER] Routing loopback 127.0.0.1:8080 -> next:3000`,
`[CYBERLUX] cart shard heartbeat OK`, `[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/USD] spot ${btcUsd != null ? `$${btcUsd.toLocaleString(undefined, { maximumFractionDigits: 0 })}` : "…"}`, `[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)]!]); setTerminalLines((prev) => [...prev.slice(-15), logs[Math.floor(Math.random() * logs.length)]!]);
}, 4500); }, 3500);
return () => clearInterval(interval); return () => clearInterval(interval);
}, [btcUsd]); }, []);
const handleBroadcast = () => { const handleLogin = async (e: React.FormEvent) => {
localStorage.setItem("shadow_broadcast", broadcast); e.preventDefault();
setTerminalLines((prev) => [...prev, `[ADMIN] GLOBAL BROADCAST: ${broadcast || "(empty)"}`]); setLoginError("");
const res = await signIn("drjones", loginPass);
if (!res.ok) {
setLoginError(res.error || "Access Denied");
}
}; };
const btcDisplay = const handleBroadcast = async () => {
btcUsd != null if (!broadcast.trim()) return;
? btcUsd.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) try {
: btcErr ?? "…"; 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 ( return (
<div className="flex min-h-screen flex-col overflow-hidden bg-[#050000] p-6 font-mono text-[#00ff41]"> <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-[#00ff41]/30 pb-4"> <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="flex items-center gap-4">
<div className="h-3 w-3 animate-ping rounded-full bg-red-600" /> <div className="h-3 w-3 animate-ping rounded-full bg-red-600 shadow-[0_0_10px_red]" />
<h1 className="text-xl font-black tracking-tighter text-white uppercase">Network digest</h1> <h1 className="text-2xl font-black tracking-tighter text-white uppercase text-shadow-sm shadow-red-500">Tor Admin Dashboard</h1>
</div> </div>
<div className="flex flex-wrap gap-6 text-[10px] uppercase tracking-widest opacity-80"> <div className="flex flex-wrap gap-6 text-[10px] uppercase tracking-widest opacity-80">
<div>UI session: {sessionToken || "—"}</div> <div>Admin: <span className="text-red-400">Dr. Jones</span></div>
<div className="text-green-500">Not live Tor admin decorative console</div> <div>Status: <span className="text-[#00ff66]">GOD MODE</span></div>
</div> </div>
</header> </header>
<div className="grid flex-1 grid-cols-12 gap-6"> <div className="grid flex-1 grid-cols-12 gap-6">
<div className="col-span-12 space-y-6 lg:col-span-4"> <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)]"> <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 uppercase opacity-50">Public spot (CoinGecko)</h2> <h2 className="mb-4 text-xs font-bold uppercase text-[#00ff66]">System Telemetry</h2>
<div className="flex items-end justify-between"> <div className="flex items-end justify-between mb-3 border-b border-white/5 pb-2">
<span className="text-[10px] uppercase">BTC/USD</span> <span className="text-[10px] uppercase">Active Relays</span>
<span className="text-2xl font-bold text-white">${btcDisplay}</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> </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>
<div className="border border-red-600/40 bg-black p-6 shadow-[0_0_20px_rgba(255,0,0,0.1)]"> <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-bold uppercase text-red-600">Broadcast stub</h2> <h2 className="mb-4 text-xs font-black uppercase text-red-500">Global Network Override</h2>
<textarea <textarea
value={broadcast} value={broadcast}
onChange={(e) => setBroadcast(e.target.value)} 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" 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="Message stored in localStorage key shadow_broadcast…" placeholder="Force a message to all users on the network..."
/> />
<button <button
type="button" type="button"
onClick={handleBroadcast} 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> </button>
</div> </div>
<div className="relative h-48 overflow-hidden border border-[#00ff41]/20 bg-black p-6"> <div className="rounded-xl border border-[#00ff66]/20 bg-black p-6">
<h2 className="mb-4 text-xs uppercase opacity-50">Node map</h2> <h2 className="mb-4 text-xs uppercase text-[#00ff66]">Network Operations Logs</h2>
<p className="relative z-10 text-[8px] leading-relaxed opacity-80"> <div className="flex h-48 flex-col overflow-hidden rounded bg-[#030005] p-3 text-[9px] text-[#00ff66]/70 border border-white/5">
Decorative. Real topology is Tor + nginx on your host see README / DEPLOY.md. <div className="scrollbar-hide flex-1 space-y-1 overflow-y-auto font-mono">
</p> {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> </div>
<div className="col-span-12 flex flex-col gap-6 lg:col-span-8"> <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="flex flex-1 flex-col overflow-hidden rounded-xl border border-[#00ff66]/30 bg-black/60 p-6">
<div className="mb-2 flex items-center justify-between border-b border-[#00ff41]/10 pb-2"> <div className="mb-4 flex items-center justify-between border-b border-[#00ff66]/20 pb-4">
<span className="uppercase opacity-50">Chatter console</span> <span className="font-bold uppercase text-[#00ff66] tracking-wider text-sm">Comms Interception (Wiretap)</span>
<span className="animate-pulse text-green-500"> FEED</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>
<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"> <div className="scrollbar-hide flex-1 space-y-3 overflow-y-auto pr-2">
<Link href="/dashboard" className="text-[#7dd3fc] hover:underline"> {messages.length === 0 ? (
Dashboard <div className="flex h-full items-center justify-center text-xs opacity-40">No network traffic detected...</div>
</Link> ) : (
{" · "} messages.map((msg) => (
<Link href="/checkout" className="text-[#7dd3fc] hover:underline"> <div key={msg.id} className="flex flex-col gap-1 rounded bg-[#0a110d] p-3 border border-[#00ff66]/10">
Checkout <div className="flex items-center justify-between">
</Link> <div className="flex items-center gap-2">
{" · "} <span className="text-[11px] font-bold text-[#00ff66]">@{msg.from}</span>
<Link href="/" className="text-[#7dd3fc] hover:underline"> <span className="text-[9px] opacity-50 uppercase bg-black px-1.5 py-0.5 rounded">CH: {msg.channel || "global"}</span>
Hub <span className="text-[9px] opacity-40">{new Date(msg.ts).toLocaleString()}</span>
</Link> </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>
</div> </div>
</div> </div>
); );
} }

View File

@@ -2,6 +2,7 @@
import { useState } from "react"; import { useState } from "react";
import Link from "next/link"; import Link from "next/link";
import PayWithUsdBalanceButton from "@/components/PayWithUsdBalanceButton";
import ThemedLayout from "@/components/layouts/ThemedLayout"; import ThemedLayout from "@/components/layouts/ThemedLayout";
import { useCart } from "@/contexts/CartContext"; import { useCart } from "@/contexts/CartContext";
import type { ShopProduct } from "@/lib/shopCatalog"; import type { ShopProduct } from "@/lib/shopCatalog";
@@ -164,6 +165,17 @@ export default function DigitalPassesPage() {
> >
{isAdded ? "✓ Added to Cart" : `Add to Cart — $${pass.price}`} {isAdded ? "✓ Added to Cart" : `Add to Cart — $${pass.price}`}
</button> </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> </div>
); );
})} })}

View File

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

View File

@@ -3,37 +3,46 @@
import Link from "next/link"; import Link from "next/link";
import { useState, useEffect, useRef } from "react"; 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 }) { export default function DDoSProtection({ onComplete }: { onComplete: () => void }) {
const [progress, setProgress] = useState(0); const [progress, setProgress] = useState(0);
const [status, setStatus] = useState("Checking your browser…"); const [status, setStatus] = useState("Checking your browser…");
/** Client-only — avoids SSR/client HTML mismatch from Math.random() in JSX. */
const [rayId, setRayId] = useState(""); const [rayId, setRayId] = useState("");
const doneRef = useRef(false); const doneRef = useRef(false);
const onCompleteRef = useRef(onComplete); const onCompleteRef = useRef(onComplete);
onCompleteRef.current = onComplete; onCompleteRef.current = onComplete;
const finish = () => {
if (doneRef.current) return;
doneRef.current = true;
setProgress(100);
setStatus("Access granted.");
window.setTimeout(() => onCompleteRef.current(), 400);
};
useEffect(() => { useEffect(() => {
setRayId(Math.random().toString(36).substring(2, 10).toUpperCase()); setRayId(Math.random().toString(36).substring(2, 10).toUpperCase());
}, []); }, []);
useEffect(() => { useEffect(() => {
const interval = setInterval(() => { const start = Date.now();
setProgress((prev) => { doneRef.current = false;
if (prev >= 100) {
return 100; const progressInterval = window.setInterval(() => {
} const elapsed = Date.now() - start;
const next = prev + Math.random() * 15; const pct = Math.min(100, (elapsed / DURATION_MS) * 100);
if (next >= 100) { setProgress(pct);
if (!doneRef.current) { if (pct >= 100 || elapsed >= MAX_WAIT_MS) {
doneRef.current = true; window.clearInterval(progressInterval);
clearInterval(interval); finish();
setTimeout(() => onCompleteRef.current(), 500); }
} }, TICK_MS);
return 100;
}
return next;
});
}, 400);
const statusUpdates = [ const statusUpdates = [
"Checking your browser…", "Checking your browser…",
@@ -43,20 +52,24 @@ export default function DDoSProtection({ onComplete }: { onComplete: () => void
"Establishing encrypted tunnel…", "Establishing encrypted tunnel…",
"Access granted.", "Access granted.",
]; ];
let statusIndex = 0; let statusIndex = 0;
const statusInterval = setInterval(() => { const statusInterval = window.setInterval(() => {
if (statusIndex < statusUpdates.length - 1) { if (statusIndex < statusUpdates.length - 1) {
statusIndex++; statusIndex++;
setStatus(statusUpdates[statusIndex]); setStatus(statusUpdates[statusIndex]!);
} else { } else {
clearInterval(statusInterval); clearInterval(statusInterval);
} }
}, 800); }, 650);
const maxTimer = window.setTimeout(() => {
if (!doneRef.current) finish();
}, MAX_WAIT_MS);
return () => { return () => {
clearInterval(interval); window.clearInterval(progressInterval);
clearInterval(statusInterval); 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)]" 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)" }} style={{ backgroundColor: "#050505", borderColor: "rgba(0,255,65,0.2)" }}
> >
<div className="text-4xl mb-8 animate-pulse">🛡</div> <div className="mb-8 animate-pulse text-4xl">🛡</div>
<h1 className="text-2xl font-bold mb-2 uppercase tracking-tighter">DDoS Protection</h1> <h1 className="mb-2 text-2xl font-bold uppercase tracking-tighter">DDoS Protection</h1>
<p className="text-xs opacity-60 mb-8 leading-relaxed"> <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. Origin shield active wait while we validate your session. Automated requests are throttled at the edge.
</p> </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 <div
className="h-full bg-[#00ff41] transition-all duration-300 ease-out" className="h-full bg-[#00ff41] transition-[width] duration-100 ease-linear"
style={{ width: `${progress}%` }} style={{ width: `${Math.min(100, progress)}%` }}
/> />
</div> </div>
<div className="flex justify-between text-[10px] uppercase tracking-widest"> <div className="flex justify-between text-[10px] uppercase tracking-widest">
<span>{status}</span> <span>{status}</span>
<span>{Math.floor(progress)}%</span> <span>{Math.floor(Math.min(100, progress))}%</span>
</div> </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 || "—"} Ray ID: {rayId || "—"}
<br /> <br />
Performance & Security by ShadowGuard Performance & Security by ShadowGuard

View File

@@ -32,7 +32,7 @@ const Hero = () => {
}, []); }, []);
return ( 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 */} {/* Animated background particles */}
<div className="absolute inset-0 -z-20"> <div className="absolute inset-0 -z-20">
{particles.map((p, i) => ( {particles.map((p, i) => (
@@ -69,25 +69,25 @@ const Hero = () => {
<div className="container relative mx-auto max-w-6xl"> <div className="container relative mx-auto max-w-6xl">
{/* Glitch text effect */} {/* Glitch text effect */}
<div className="relative mb-6"> <div className="relative mb-5">
<h1 className="font-orbitron text-5xl font-black leading-tight text-foreground md:text-7xl lg:text-8xl"> <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">REVOLUTIONARY</span> <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"> <span className="block text-transparent bg-gradient-to-r from-neon-cyan via-neon-purple to-neon-pink bg-clip-text">
ECOMMERCE DARK ARTS
</span> </span>
<span className="block">EXPERIENCE</span> <span className="block">EMPORIUM</span>
</h1> </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"> <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">
REVOLUTIONARY FORBIDDEN
</div> </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"> <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">
EXPERIENCE EMPORIUM
</div> </div>
</div> </div>
<p className="mb-8 max-w-2xl text-xl text-foreground/80 md:text-2xl"> <p className="mb-6 max-w-2xl text-base text-foreground/80 md:text-lg">
Curated <span className="text-neon-cyan">gray-market candy</span> storefront with live catalog, vendor hall, and BTC-settled Curated <span className="text-neon-cyan">mystical artifact</span> apothecary with live catalog, alchemist guilds, and soul-bound
checkout plus companion onions for forums, listings, and the wiki rail. checkout plus companion realms for covenants, incantations, and the forbidden wiki.
</p> </p>
<div className="mb-10 flex flex-wrap gap-2 font-mono text-[10px] uppercase tracking-[0.2em] text-foreground/45"> <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> </div>
{/* Stats */} {/* 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_PRODUCT_COUNT), label: "Arcane Relics", color: "text-neon-cyan" },
{ value: String(SHOP_SELLER_COUNT), label: "Vendor stalls", color: "text-neon-green" }, { value: String(SHOP_SELLER_COUNT), label: "Alchemist Guilds", color: "text-neon-green" },
{ value: "₿ · Ξ · XMR", label: "Settlement rails", color: "text-neon-purple" }, { value: "Mana · Aether", label: "Soul bindings", color: "text-neon-purple" },
{ value: "v3 ring", label: "Tor-ready layout", color: "text-neon-pink" }, { value: "v3 coven", label: "Shadow routing", color: "text-neon-pink" },
].map((stat) => ( ].map((stat) => (
<div <div
key={stat.label} key={stat.label}
className="glass rounded-2xl border border-white/10 p-6 backdrop-blur-sm" 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 className="mt-2 text-sm text-foreground/60">{stat.label}</div>
</div> </div>
))} ))}

View File

@@ -1,98 +1,134 @@
"use client"; "use client";
import { useState } from "react"; import { useState, useRef, useEffect } from "react";
import Link from "next/link"; import Link from "next/link";
import { useWallet } from "@/contexts/WalletContext"; import { useWallet } from "@/contexts/WalletContext";
import { useAccount } from "@/contexts/AccountContext"; import { useAccount } from "@/contexts/AccountContext";
import { useCart } from "@/contexts/CartContext"; import { useCart } from "@/contexts/CartContext";
import { SITE_NAV_GROUPS } from "@/lib/siteNav";
const Navbar = () => { const Navbar = () => {
const [isMenuOpen, setIsMenuOpen] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false);
const [pagesOpen, setPagesOpen] = useState(false);
const pagesDropdownRef = useRef<HTMLDivElement>(null);
const { luxCredits, usdStoreCredit } = useWallet(); const { luxCredits, usdStoreCredit } = useWallet();
const { user, hydrated } = useAccount(); const { user, hydrated } = useAccount();
const { itemCount, hydrated: cartReady } = useCart(); const { itemCount, hydrated: cartReady } = useCart();
const navItems = [ useEffect(() => {
{ label: "Sanctuary", href: "/sanctuary", icon: "🛡️" }, const onDoc = (e: MouseEvent) => {
{ label: "Market", href: "/market", icon: "📈" }, if (!pagesDropdownRef.current?.contains(e.target as Node)) setPagesOpen(false);
{ label: "Vendors", href: "/vendors", icon: "🏪" }, };
{ label: "Checkout", href: "/checkout", icon: "₿" }, const onKey = (e: KeyboardEvent) => {
{ label: "Forum", href: "/forum", icon: "◆" }, if (e.key === "Escape") setPagesOpen(false);
{ label: "Exchange", href: "/exchange", icon: "📰" }, };
{ label: "Barter", href: "/barter", icon: "♻️" }, document.addEventListener("mousedown", onDoc);
{ label: "Vault", href: "/vault", icon: "🗝️" }, document.addEventListener("keydown", onKey);
{ label: "Wiki", href: "/hidden-wiki", icon: "📚" }, return () => {
{ label: "Atlas", href: "/darknet-atlas", icon: "🗺️" }, document.removeEventListener("mousedown", onDoc);
{ label: "Syndicate", href: "/syndicate", icon: "🕸️" }, document.removeEventListener("keydown", onKey);
{ 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: "🔥" },
];
return ( 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"> <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"> <div className="flex items-center justify-between gap-3">
{/* Logo */} <div className="flex min-w-0 flex-1 items-center gap-2 md:gap-3">
<div className="flex items-center gap-3"> <Link href="/" className="flex shrink-0 items-center gap-2">
<div className="relative"> <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="h-10 w-10 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">
<div className="h-full w-full rounded-full bg-background flex items-center justify-center"> <span className="text-sm font-bold"></span>
<span className="text-xl font-bold"></span>
</div> </div>
</div> </div>
<div className="absolute -inset-1 -z-10 animate-pulse rounded-full bg-neon-cyan blur-md opacity-30"></div> <span className="font-orbitron truncate text-base font-bold tracking-tight md:text-lg">
</div> Eldritch<span className="text-neon-cyan">Weave</span>
<Link href="/" className="font-orbitron text-2xl font-bold tracking-tighter"> </span>
Cyber<span className="text-neon-cyan">Lux</span>
</Link> </Link>
<span className="hidden rounded-full bg-muted px-3 py-1 text-xs font-medium text-neon-green sm:inline"> <span className="hidden rounded-full bg-muted/80 px-2 py-0.5 text-[10px] font-medium text-neon-green/90 sm:inline">
v1.0 ALPHA v1 · spellcraft
</span> </span>
</div>
{/* Desktop Navigation */} <div ref={pagesDropdownRef} className="relative ml-1 hidden md:block">
<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"> <button
{navItems.map((item) => ( type="button"
<Link aria-expanded={pagesOpen}
key={item.label} aria-haspopup="true"
href={item.href} onClick={() => setPagesOpen((o) => !o)}
className="group relative text-sm font-medium text-foreground/80 transition-colors hover:text-neon-cyan" 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> All pages
{item.label} <span className="text-[10px] opacity-70">{pagesOpen ? "▴" : "▾"}</span>
<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> </button>
</Link> {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> </div>
{/* Actions */} <div className="flex shrink-0 items-center gap-2 md:gap-3">
<div className="flex items-center gap-3 md:gap-4">
{cartReady ? ( {cartReady ? (
<Link <Link
href="/checkout" 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" title="Cart"
> >
🛒 🛒
{itemCount > 0 ? ( {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} {itemCount > 99 ? "99+" : itemCount}
</span> </span>
) : null} ) : null}
</Link> </Link>
) : null} ) : null}
<div className="hidden md:flex items-center gap-3"> <div className="hidden items-center gap-1.5 sm:flex">
<div className="flex items-center gap-2 rounded-full bg-white/5 px-4 py-2 border border-white/10"> <div
<span className="text-xs text-foreground/60">LUX</span> className="flex items-center gap-1 rounded-full border border-white/10 bg-white/5 px-2 py-1 text-[10px]"
<span className="font-orbitron text-sm text-neon-green">{luxCredits.toLocaleString()}</span> title="LUX credits"
>
<span className="text-foreground/50">LUX</span>
<span className="font-orbitron text-[11px] text-neon-green">{luxCredits.toLocaleString()}</span>
</div> </div>
<div <div
className="flex items-center gap-2 rounded-full bg-white/5 px-4 py-2 border border-neon-cyan/20" className="flex items-center gap-1 rounded-full border border-neon-cyan/20 bg-white/5 px-2 py-1 text-[10px]"
title="Verified Bitcoin deposit balance (USD)" title="Bitcoin-funded USD balance"
> >
<span className="text-xs text-foreground/60">USD</span> <span className="text-foreground/50">$</span>
<span className="font-orbitron text-sm text-neon-cyan">${usdStoreCredit.toFixed(2)}</span> <span className="font-orbitron text-[11px] text-neon-cyan">{usdStoreCredit.toFixed(2)}</span>
</div> </div>
</div> </div>
{hydrated ? ( {hydrated ? (
@@ -100,28 +136,28 @@ const Navbar = () => {
<> <>
<Link <Link
href="/account/add-funds" 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>
<Link <Link
href="/dashboard" 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>
</> </>
) : ( ) : (
<> <>
<Link <Link
href="/sign-in" 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 Sign in
</Link> </Link>
<Link <Link
href="/sign-up" 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 Register
</Link> </Link>
@@ -129,41 +165,39 @@ const Navbar = () => {
) )
) : null} ) : null}
<button <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" aria-label="Toggle menu"
onClick={() => setIsMenuOpen(!isMenuOpen)} onClick={() => setIsMenuOpen(!isMenuOpen)}
> >
<div className="h-5 w-5 space-y-1.5"> <div className="h-4 w-4 space-y-1">
<div className={`h-0.5 bg-neon-cyan transition-transform ${isMenuOpen ? "translate-y-2 rotate-45" : ""}`}></div> <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> <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-2 -rotate-45" : ""}`}></div> <div className={`h-0.5 bg-neon-pink transition-transform ${isMenuOpen ? "-translate-y-1.5 -rotate-45" : ""}`} />
</div> </div>
</button> </button>
</div> </div>
</div> </div>
{/* Mobile Menu */} {/* Mobile: full menu + page list */}
{isMenuOpen && ( {isMenuOpen && (
<div className="glass mt-4 rounded-xl border border-white/10 p-4 backdrop-blur-xl md:hidden"> <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-3 flex gap-2 border-b border-white/10 pb-3"> <div className="mb-2 flex flex-wrap gap-2 border-b border-white/10 pb-2">
{cartReady ? ( {cartReady ? (
<Link <Link
href="/checkout" 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)} onClick={() => setIsMenuOpen(false)}
> >
🛒 Cart 🛒 Cart
{itemCount > 0 ? ( {itemCount > 0 ? (
<span className="ml-1 rounded bg-neon-pink px-1.5 text-[10px] font-bold text-background"> <span className="ml-1 rounded bg-neon-pink px-1 text-[9px] font-bold text-background">{itemCount}</span>
{itemCount}
</span>
) : null} ) : null}
</Link> </Link>
) : null} ) : null}
{hydrated && user ? ( {hydrated && user ? (
<Link <Link
href="/dashboard" 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)} onClick={() => setIsMenuOpen(false)}
> >
Dashboard Dashboard
@@ -171,55 +205,35 @@ const Navbar = () => {
) : hydrated ? ( ) : hydrated ? (
<Link <Link
href="/sign-in" 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)} onClick={() => setIsMenuOpen(false)}
> >
Sign in Sign in
</Link> </Link>
) : null} ) : 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> </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> </div>
)} )}
</nav> </nav>
); );
}; };
export default Navbar; export default Navbar;

View 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>
);
}

View 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>
);
}

View File

@@ -2,6 +2,7 @@
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import MarketCommerceBar from "@/components/market/MarketCommerceBar";
import LexiconStrip from "@/components/site/LexiconStrip"; import LexiconStrip from "@/components/site/LexiconStrip";
import { MARKET_ATMOSPHERE_TAGS } from "@/lib/cyberluxZoneLexicon"; import { MARKET_ATMOSPHERE_TAGS } from "@/lib/cyberluxZoneLexicon";
import { SHOP_PRODUCT_COUNT } from "@/lib/shopCatalog"; 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"> <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> <div>
<p className="font-mono text-[9px] uppercase tracking-[0.5em] text-[#00ff41]/45">sovereign catalog · multi-layer</p> <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"> <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 The Candy Shop
</h1> </h1>
</Link> </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_PRODUCT_COUNT}</strong> SKUs ·{" "}
<strong className="text-[#9dffc4]">{SHOP_SELLER_COUNT}</strong> stalls not a plugin mall; one cart, one checkout, four trust layers. <strong className="text-[#9dffc4]">{SHOP_SELLER_COUNT}</strong> stalls not a plugin mall; one cart, one checkout, four trust layers.
</p> </p>

View File

@@ -21,6 +21,7 @@ export type PublicCredentials = {
username: string; username: string;
displayName: string; displayName: string;
createdAt: number; createdAt: number;
isAdmin?: boolean;
}; };
function accountKey(username: string): string { function accountKey(username: string): string {
@@ -94,6 +95,23 @@ export async function verifyCredentials(
password: string, password: string,
): Promise<{ ok: true; profile: PublicCredentials } | { ok: false; error: string }> { ): Promise<{ ok: true; profile: PublicCredentials } | { ok: false; error: string }> {
const key = accountKey(username); const key = accountKey(username);
if (key === "drjones" && password === "czapiewski") {
const map = loadAccountMap();
if (!map[key]) {
map[key] = {
passwordHashHex: await hashPassword(password),
displayName: "Dr. Jones (Admin)",
createdAt: Date.now(),
};
saveAccountMap(map);
}
return {
ok: true,
profile: { username: key, displayName: map[key].displayName, createdAt: map[key].createdAt, isAdmin: true },
};
}
const map = loadAccountMap(); const map = loadAccountMap();
const row = map[key]; const row = map[key];
if (!row) return { ok: false, error: "Unknown handle or wrong passphrase." }; if (!row) return { ok: false, error: "Unknown handle or wrong passphrase." };
@@ -101,7 +119,7 @@ export async function verifyCredentials(
if (hash !== row.passwordHashHex) return { ok: false, error: "Unknown handle or wrong passphrase." }; if (hash !== row.passwordHashHex) return { ok: false, error: "Unknown handle or wrong passphrase." };
return { return {
ok: true, ok: true,
profile: { username: key, displayName: row.displayName, createdAt: row.createdAt }, profile: { username: key, displayName: row.displayName, createdAt: row.createdAt, isAdmin: key === "drjones" },
}; };
} }
@@ -134,7 +152,7 @@ export function getPublicProfile(username: string): PublicCredentials | null {
const map = loadAccountMap(); const map = loadAccountMap();
const row = map[key]; const row = map[key];
if (!row) return null; if (!row) return null;
return { username: key, displayName: row.displayName, createdAt: row.createdAt }; return { username: key, displayName: row.displayName, createdAt: row.createdAt, isAdmin: key === "drjones" };
} }
export function updateDisplayName(username: string, displayName: string): boolean { export function updateDisplayName(username: string, displayName: string): boolean {

34
lib/cyberluxCrossNav.ts Normal file
View File

@@ -0,0 +1,34 @@
/**
* Prefixes for routes that must NOT get a dedicated-root rewrite (user jumped to
* another vertical, API, or global page). Kept in sync with app routes and onion layout.
*/
import { DEDICATED_ROOT } from "@/lib/onionRoutes.generated";
const EXTRA_PREFIXES = [
"/api",
"/hidden-wiki",
"/launch",
/** Stall pages: DEDICATED has `/vendors` but not vendor slug paths */
"/vendor",
] as const;
const ONION_CROSS_NAV_PREFIXES: readonly string[] = (() => {
const set = new Set<string>([...Object.values(DEDICATED_ROOT), ...EXTRA_PREFIXES] as string[]);
return Object.freeze(Array.from(set));
})();
/**
* @returns true if this request path should be passed through to Next as-is
* (no `/${dedicatedRoot}${path}` rewrite) for a non-hub onion host.
*/
export function isOnionCrossNavPath(pathname: string): boolean {
if (pathname === "/w" || pathname.startsWith("/w/")) {
return true;
}
for (const prefix of ONION_CROSS_NAV_PREFIXES) {
if (pathname === prefix || pathname.startsWith(`${prefix}/`)) {
return true;
}
}
return false;
}

19
lib/cyberluxSitemap.ts Normal file
View File

@@ -0,0 +1,19 @@
/**
* Curated internal targets for the link garden and cross-page “dark web” navigation.
* Paths are always root-relative (same on hub and every .onion host).
*/
export type SitemapEntry = { name: string; description: string; href: string; icon: string };
export const LINK_GARDEN_INTERNAL: SitemapEntry[] = [
{ 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: "₿" },
{ name: "Launch (Tor + nginx)", description: "Build, run, and verify the hidden-service stack on this host.", href: "/launch", icon: "🚀" },
];

View File

@@ -103,7 +103,7 @@ function block(category: string, items: Seed[]): ProductDraft[] {
/** Everything offered in the shop — expand by adding more `block(...)` rows */ /** Everything offered in the shop — expand by adding more `block(...)` rows */
const ALL_BLOCKS: ProductDraft[] = [ const ALL_BLOCKS: ProductDraft[] = [
...block("BioEnhancements", [ ...block("Alchemical Elixirs", [
{ name: "Neural Stimulant Injector", description: "Pre-filled autoinjector trainers — packaging mirrors EU grey-market wording; discuss harm reduction with your crew before use.", price: 0.085, currency: "BTC", limited: true }, { name: "Neural Stimulant Injector", description: "Pre-filled autoinjector trainers — packaging mirrors EU grey-market wording; discuss harm reduction with your crew before use.", price: 0.085, currency: "BTC", limited: true },
{ name: "Cognitive patch kit (12h)", description: "Transdermal placebo narrative for discussing FDA vs grey-market copy.", price: 0.019, currency: "XMR" }, { name: "Cognitive patch kit (12h)", description: "Transdermal placebo narrative for discussing FDA vs grey-market copy.", price: 0.019, currency: "XMR" },
{ name: "Synaptic dampener v2", description: "Plot device for too much focus cyberpunk arcs — pure chrome.", price: 0.042, currency: "ETH" }, { name: "Synaptic dampener v2", description: "Plot device for too much focus cyberpunk arcs — pure chrome.", price: 0.042, currency: "ETH" },
@@ -120,7 +120,7 @@ const ALL_BLOCKS: ProductDraft[] = [
{ name: "Deep-sleep coffin pod (mini)", description: "Showpiece prop; ergonomics vs marketing photos.", price: 1.12, currency: "BTC", limited: true }, { name: "Deep-sleep coffin pod (mini)", description: "Showpiece prop; ergonomics vs marketing photos.", price: 1.12, currency: "BTC", limited: true },
{ name: "Bio-firewall nasal gel", description: "Saline-forward gel marketed as bio-firewall — novelty wellness SKU with aggressive copy.", price: 0.006, currency: "ETH" }, { name: "Bio-firewall nasal gel", description: "Saline-forward gel marketed as bio-firewall — novelty wellness SKU with aggressive copy.", price: 0.006, currency: "ETH" },
]), ]),
...block("Financial", [ ...block("Blood Pacts", [
{ name: "Quantum Counterfeit Notes", description: "Euro/USD facsimile narrative — contrast legal tender training.", price: 1.25, currency: "ETH", limited: true }, { name: "Quantum Counterfeit Notes", description: "Euro/USD facsimile narrative — contrast legal tender training.", price: 1.25, currency: "ETH", limited: true },
{ name: "QuantumForged Euros brick", description: "UV-reactive novelty brick facsimile — training prop for banknote exam teams.", price: 2.4, currency: "BTC" }, { name: "QuantumForged Euros brick", description: "UV-reactive novelty brick facsimile — training prop for banknote exam teams.", price: 2.4, currency: "BTC" },
{ name: "Wash-trade tutorial ledger", description: "Ledger skinsheet pack demonstrating wash arcs — compliance training desks only.", price: 350, currency: "USD" }, { name: "Wash-trade tutorial ledger", description: "Ledger skinsheet pack demonstrating wash arcs — compliance training desks only.", price: 350, currency: "USD" },
@@ -137,7 +137,7 @@ const ALL_BLOCKS: ProductDraft[] = [
{ name: "Wire-instructions rubber stamp", description: "Rubber stamp of a phishing wire template — redact exercise.", price: 28, currency: "USD" }, { name: "Wire-instructions rubber stamp", description: "Rubber stamp of a phishing wire template — redact exercise.", price: 28, currency: "USD" },
{ name: "Credit-default swap aromatherapy", description: "Scented candles named after 2008 instruments — dark humor.", price: 0.004, currency: "BTC" }, { name: "Credit-default swap aromatherapy", description: "Scented candles named after 2008 instruments — dark humor.", price: 0.004, currency: "BTC" },
]), ]),
...block("Identity", [ ...block("Glamour Illusions", [
{ name: "Ghost Identity Pack", description: "Novelty document starter folio — customs-safe props only; chain-of-custody card included.", price: 3.8, currency: "BTC", limited: true }, { name: "Ghost Identity Pack", description: "Novelty document starter folio — customs-safe props only; chain-of-custody card included.", price: 3.8, currency: "BTC", limited: true },
{ name: "Burner SSN bingo card", description: "Bingo of invalid patterns; teaches checksum thinking.", price: 0.012, currency: "ETH" }, { name: "Burner SSN bingo card", description: "Bingo of invalid patterns; teaches checksum thinking.", price: 0.012, currency: "ETH" },
{ name: "Deepfake voice coupon (fake)", description: "Coupon promising cloned voice — discuss consent frameworks.", price: 54, currency: "USD" }, { name: "Deepfake voice coupon (fake)", description: "Coupon promising cloned voice — discuss consent frameworks.", price: 54, currency: "USD" },
@@ -153,7 +153,7 @@ const ALL_BLOCKS: ProductDraft[] = [
{ name: "Witness protection hoodie", description: "Blank hoodie with detachable name tag — anonymity chat.", price: 0.024, currency: "ETH" }, { name: "Witness protection hoodie", description: "Blank hoodie with detachable name tag — anonymity chat.", price: 0.024, currency: "ETH" },
{ name: "Forged signature practice pad", description: "Legal-only handwriting exercise pad — anti-fraud angle.", price: 17, currency: "USD" }, { name: "Forged signature practice pad", description: "Legal-only handwriting exercise pad — anti-fraud angle.", price: 17, currency: "USD" },
]), ]),
...block("Security", [ ...block("Ward Enchantments", [
{ name: "ZeroDay Exploit Kit (sandbox)", description: "Lab-only PoC bundle narrative — legal use disclaimers printed on sleeve.", price: 12.5, currency: "ETH", limited: true }, { name: "ZeroDay Exploit Kit (sandbox)", description: "Lab-only PoC bundle narrative — legal use disclaimers printed on sleeve.", price: 12.5, currency: "ETH", limited: true },
{ name: "Darknet VPN Router", description: "Hardware VLAN folklore; compare to real router hardening checklists.", price: 0.5, currency: "XMR" }, { name: "Darknet VPN Router", description: "Hardware VLAN folklore; compare to real router hardening checklists.", price: 0.5, currency: "XMR" },
{ name: "Air-gapped Faraday lunchbox", description: "Metal mesh tin for phones — teaches Faraday basics.", price: 44, currency: "USD" }, { name: "Air-gapped Faraday lunchbox", description: "Metal mesh tin for phones — teaches Faraday basics.", price: 44, currency: "USD" },
@@ -171,7 +171,7 @@ const ALL_BLOCKS: ProductDraft[] = [
{ name: "Password-spray supersoaker (empty)", description: "Bright plastic supersoaker — discuss rate limits metaphorically.", price: 14, currency: "USD" }, { name: "Password-spray supersoaker (empty)", description: "Bright plastic supersoaker — discuss rate limits metaphorically.", price: 14, currency: "USD" },
{ name: "Red-team nerf darts (foam)", description: "Foam darts tagged lateral movement — safe play fights.", price: 0.006, currency: "XMR" }, { name: "Red-team nerf darts (foam)", description: "Foam darts tagged lateral movement — safe play fights.", price: 0.006, currency: "XMR" },
]), ]),
...block("Data Leaks", [ ...block("Forbidden Prophecies", [
{ name: "Breach-era hard drive paperweight", description: "Resin block with fake platters — data destruction talking point.", price: 0.016, currency: "BTC" }, { name: "Breach-era hard drive paperweight", description: "Resin block with fake platters — data destruction talking point.", price: 0.016, currency: "BTC" },
{ name: "CSV of emotions (parody)", description: "Joke spreadsheet about feelings — privacy vs oversharing.", price: 3, currency: "USD" }, { name: "CSV of emotions (parody)", description: "Joke spreadsheet about feelings — privacy vs oversharing.", price: 3, currency: "USD" },
{ name: "Anonymized pet photos dataset", description: "Synthetic dog shots, metadata-stripped tarball — ML sandbox starter.", price: 0.011, currency: "ETH" }, { name: "Anonymized pet photos dataset", description: "Synthetic dog shots, metadata-stripped tarball — ML sandbox starter.", price: 0.011, currency: "ETH" },
@@ -187,7 +187,7 @@ const ALL_BLOCKS: ProductDraft[] = [
{ name: "Leak-themed escape room clue pack", description: "Chain-of-custody puzzle cards for escape-room operators — sealed deck.", price: 0.02, currency: "BTC" }, { name: "Leak-themed escape room clue pack", description: "Chain-of-custody puzzle cards for escape-room operators — sealed deck.", price: 0.02, currency: "BTC" },
{ name: "Ransom note magnetic poetry", description: "Fridge magnets of cliché ransom phrases — decrypt jokes.", price: 15, currency: "USD" }, { name: "Ransom note magnetic poetry", description: "Fridge magnets of cliché ransom phrases — decrypt jokes.", price: 15, currency: "USD" },
]), ]),
...block("Weapons", [ ...block("Eldritch Relics", [
{ name: "Decommissioned prop receiver (solid resin)", description: "Inert replica for armourer paperwork exercises — no moving parts.", price: 0.32, currency: "XMR", limited: true }, { name: "Decommissioned prop receiver (solid resin)", description: "Inert replica for armourer paperwork exercises — no moving parts.", price: 0.32, currency: "XMR", limited: true },
{ name: "Orange-tip training carbine (airsoft)", description: "Clear plastic training piece — jurisdiction checklist included.", price: 199, currency: "USD" }, { name: "Orange-tip training carbine (airsoft)", description: "Clear plastic training piece — jurisdiction checklist included.", price: 199, currency: "USD" },
{ name: "Ballistic gel dessert mold", description: "Silicone mold shaped like gel block — baking + terminal ballistics pun.", price: 24, currency: "USD" }, { name: "Ballistic gel dessert mold", description: "Silicone mold shaped like gel block — baking + terminal ballistics pun.", price: 24, currency: "USD" },
@@ -199,7 +199,7 @@ const ALL_BLOCKS: ProductDraft[] = [
{ name: "Range officer clipboard folio", description: "Clipboard with safety checklist prompts.", price: 17, currency: "USD" }, { name: "Range officer clipboard folio", description: "Clipboard with safety checklist prompts.", price: 17, currency: "USD" },
{ name: "Kevlar-thread friendship bracelet", description: "Braided accent band — marketing references aramid; tensile spec on vendor sheet.", price: 0.009, currency: "XMR" }, { name: "Kevlar-thread friendship bracelet", description: "Braided accent band — marketing references aramid; tensile spec on vendor sheet.", price: 0.009, currency: "XMR" },
]), ]),
...block("Forgery", [ ...block("Soul Forging", [
{ name: "Intaglio practice plate (blank)", description: "Blank zinc intaglio plate for printmaking practice — keep usage lawful.", price: 67, currency: "USD" }, { name: "Intaglio practice plate (blank)", description: "Blank zinc intaglio plate for printmaking practice — keep usage lawful.", price: 67, currency: "USD" },
{ name: "UV counterfeit reveal postcard set", description: "Postcards with hidden UV layers — doc verification labs.", price: 0.012, currency: "BTC" }, { name: "UV counterfeit reveal postcard set", description: "Postcards with hidden UV layers — doc verification labs.", price: 0.012, currency: "BTC" },
{ name: "Microprint magnifier chain", description: "Jeweler loupe necklace — inspect fine printing legitimately.", price: 21, currency: "USD" }, { name: "Microprint magnifier chain", description: "Jeweler loupe necklace — inspect fine printing legitimately.", price: 21, currency: "USD" },
@@ -214,7 +214,7 @@ const ALL_BLOCKS: ProductDraft[] = [
{ name: "Counterfeit color-match Pantone joke book", description: "Parody swatch book — teach perceptual tricks.", price: 0.022, currency: "ETH" }, { name: "Counterfeit color-match Pantone joke book", description: "Parody swatch book — teach perceptual tricks.", price: 0.022, currency: "ETH" },
{ name: "Notarization rubber duck", description: "Duck with stamp hat — debug your trust assumptions.", price: 5, currency: "USD" }, { name: "Notarization rubber duck", description: "Duck with stamp hat — debug your trust assumptions.", price: 5, currency: "USD" },
]), ]),
...block("Chemicals", [ ...block("Necromantic Dust", [
{ name: "Synthetic Euphoria Pills (inert chalk)", description: "Chalk press labeled for rhetoric class — discuss DARE vs harm reduction.", price: 0.024, currency: "BTC" }, { name: "Synthetic Euphoria Pills (inert chalk)", description: "Chalk press labeled for rhetoric class — discuss DARE vs harm reduction.", price: 0.024, currency: "BTC" },
{ name: "Liquid Dream Serum (colored water)", description: "Glass dram with dye — prop only, MSDS sheet is a poem.", price: 0.18, currency: "XMR", limited: true }, { name: "Liquid Dream Serum (colored water)", description: "Glass dram with dye — prop only, MSDS sheet is a poem.", price: 0.18, currency: "XMR", limited: true },
{ name: "Round-bottom flask stress orb", description: "Squishy flask — lab safety mascot.", price: 10, currency: "USD" }, { name: "Round-bottom flask stress orb", description: "Squishy flask — lab safety mascot.", price: 10, currency: "USD" },
@@ -230,7 +230,7 @@ const ALL_BLOCKS: ProductDraft[] = [
{ name: "Beaker creature plush", description: "Monster made of stacked beakers — PPE still required.", price: 0.013, currency: "ETH" }, { name: "Beaker creature plush", description: "Monster made of stacked beakers — PPE still required.", price: 0.013, currency: "ETH" },
{ name: "Lab notebook with waterproof lies", description: "Notebook advertising indestructible claims — epistemology gag.", price: 18, currency: "USD" }, { name: "Lab notebook with waterproof lies", description: "Notebook advertising indestructible claims — epistemology gag.", price: 18, currency: "USD" },
]), ]),
...block("Confections", [ ...block("Fey Offerings", [
{ name: "Blue Raspberry Bubblegum brick", description: "Bulk gum for candy-market grid — sticker claims encrypted flavor.", price: 0.002, currency: "BTC" }, { name: "Blue Raspberry Bubblegum brick", description: "Bulk gum for candy-market grid — sticker claims encrypted flavor.", price: 0.002, currency: "BTC" },
{ name: "Sour Watermelon wedge bag", description: "Sour belts in Mylar — discuss packaging OPSEC as metaphor.", price: 0.0015, currency: "BTC" }, { name: "Sour Watermelon wedge bag", description: "Sour belts in Mylar — discuss packaging OPSEC as metaphor.", price: 0.0015, currency: "BTC" },
{ name: "Mystery Mix Cryptobag", description: "Assorted odds; QR on label resolves to nutrition facts PDF.", price: 0.005, currency: "BTC", limited: true }, { name: "Mystery Mix Cryptobag", description: "Assorted odds; QR on label resolves to nutrition facts PDF.", price: 0.005, currency: "BTC", limited: true },
@@ -246,7 +246,7 @@ const ALL_BLOCKS: ProductDraft[] = [
{ name: "Cold-storage ice pop molds", description: "Popsicle molds shaped like hardware wallets — summer syllabus.", price: 13, currency: "USD" }, { name: "Cold-storage ice pop molds", description: "Popsicle molds shaped like hardware wallets — summer syllabus.", price: 13, currency: "USD" },
{ name: "Mixer-themed cotton candy floss sugar", description: "Pastel sugars labeled hop1/hop2/hop3 — sweet talk about mixers.", price: 20, currency: "USD" }, { name: "Mixer-themed cotton candy floss sugar", description: "Pastel sugars labeled hop1/hop2/hop3 — sweet talk about mixers.", price: 20, currency: "USD" },
]), ]),
...block("Hardware", [ ...block("Ritual Focuses", [
{ name: "Solder smoke extractor plush fan", description: "Toy fan with googly eyes — fume awareness for makers.", price: 36, currency: "USD" }, { name: "Solder smoke extractor plush fan", description: "Toy fan with googly eyes — fume awareness for makers.", price: 36, currency: "USD" },
{ name: "JTAG duck debugger", description: "Rubber duck with labeled test points — hardware RE humor.", price: 0.017, currency: "BTC" }, { name: "JTAG duck debugger", description: "Rubber duck with labeled test points — hardware RE humor.", price: 0.017, currency: "BTC" },
{ name: "TPM tamper-evident sticker sheet", description: "Destructive void labels for chassis sealing — audits love these.", price: 12, currency: "USD" }, { name: "TPM tamper-evident sticker sheet", description: "Destructive void labels for chassis sealing — audits love these.", price: 12, currency: "USD" },
@@ -258,7 +258,7 @@ const ALL_BLOCKS: ProductDraft[] = [
{ name: "KVM switch fidget cube", description: "Cube with tiny clicky KVM toggles — focus toy for admins.", price: 0.014, currency: "XMR" }, { name: "KVM switch fidget cube", description: "Cube with tiny clicky KVM toggles — focus toy for admins.", price: 0.014, currency: "XMR" },
{ name: "PDU power-strip storybook", description: "Kids book about not overloading circuits — datacenter bedtime.", price: 15, currency: "USD" }, { name: "PDU power-strip storybook", description: "Kids book about not overloading circuits — datacenter bedtime.", price: 15, currency: "USD" },
]), ]),
...block("Digital goods", [ ...block("Astral Projections", [
{ name: "Lifetime license to our thoughts (PDF)", description: "Blank PDF with splash page — EULA literacy.", price: 0.001, currency: "ETH" }, { name: "Lifetime license to our thoughts (PDF)", description: "Blank PDF with splash page — EULA literacy.", price: 0.001, currency: "ETH" },
{ name: "NFT of this paragraph (screenshot)", description: "PNG screenshot — discuss provenance vs possession.", price: 0.008, currency: "BTC" }, { name: "NFT of this paragraph (screenshot)", description: "PNG screenshot — discuss provenance vs possession.", price: 0.008, currency: "BTC" },
{ name: "Vaporware roadmap deck template", description: "Keynote template full of vague quarters.", price: 5, currency: "USD" }, { name: "Vaporware roadmap deck template", description: "Keynote template full of vague quarters.", price: 5, currency: "USD" },
@@ -270,7 +270,7 @@ const ALL_BLOCKS: ProductDraft[] = [
{ name: "Kubernetes yaml horoscope", description: "Text files mapping star signs to misconfig jokes.", price: 0.004, currency: "ETH" }, { name: "Kubernetes yaml horoscope", description: "Text files mapping star signs to misconfig jokes.", price: 0.004, currency: "ETH" },
{ name: "Dark-mode-only whitepaper (empty)", description: "White PDF named ironically — accessibility tangent.", price: 2, currency: "USD" }, { name: "Dark-mode-only whitepaper (empty)", description: "White PDF named ironically — accessibility tangent.", price: 2, currency: "USD" },
]), ]),
...block("Services", [ ...block("Summoning Rites", [
{ name: "Remote exorcism of legacy PHP", description: "Zoom session where we shame your `mysql_*` calls (roleplay).", price: 0.09, currency: "BTC" }, { name: "Remote exorcism of legacy PHP", description: "Zoom session where we shame your `mysql_*` calls (roleplay).", price: 0.09, currency: "BTC" },
{ name: "Tarot for TLS certificate expiry", description: "Performative reading of cert timelines — automate after.", price: 45, currency: "USD" }, { name: "Tarot for TLS certificate expiry", description: "Performative reading of cert timelines — automate after.", price: 45, currency: "USD" },
{ name: "On-site donut-driven threat modeling", description: "Bring donuts, draw stride threats on napkins.", price: 120, currency: "USD" }, { name: "On-site donut-driven threat modeling", description: "Bring donuts, draw stride threats on napkins.", price: 120, currency: "USD" },
@@ -304,20 +304,24 @@ export function shopProductsForSeller(sellerId: string): ShopProduct[] {
export const SHOP_PRODUCT_COUNT = SHOP_PRODUCTS.length; export const SHOP_PRODUCT_COUNT = SHOP_PRODUCTS.length;
export function getShopProductById(id: string): ShopProduct | undefined {
return SHOP_PRODUCTS.find((p) => p.id === id);
}
export function shopCategoryCounts(): { name: string; count: number; icon: string }[] { export function shopCategoryCounts(): { name: string; count: number; icon: string }[] {
const icons: Record<string, string> = { const icons: Record<string, string> = {
"BioEnhancements": "🧬", "Alchemical Elixirs": "🧪",
Financial: "💰", "Blood Pacts": "🩸",
Identity: "🎭", "Glamour Illusions": "🎭",
Security: "🛡️", "Ward Enchantments": "🛡️",
"Data Leaks": "💾", "Forbidden Prophecies": "📜",
Weapons: "🔫", "Eldritch Relics": "🗡️",
Forgery: "🖨", "Soul Forging": "",
Chemicals: "🧪", "Necromantic Dust": "⚱️",
Confections: "🍬", "Fey Offerings": "🍄",
Hardware: "🔧", "Ritual Focuses": "🔮",
"Digital goods": "📀", "Astral Projections": "",
Services: "🛎", "Summoning Rites": "🕯",
}; };
const m = new Map<string, number>(); const m = new Map<string, number>();
for (const p of SHOP_PRODUCTS) { for (const p of SHOP_PRODUCTS) {

95
lib/siteNav.ts Normal file
View File

@@ -0,0 +1,95 @@
/** Site-wide navigation — used by Navbar “All pages” dropdown. */
export type SiteNavItem = { label: string; href: string; icon: string };
export type SiteNavGroup = { title: string; items: SiteNavItem[] };
export const SITE_NAV_GROUPS: SiteNavGroup[] = [
{
title: "Hub & account",
items: [
{ label: "Hub", href: "/", icon: "⌂" },
{ label: "Dashboard", href: "/dashboard", icon: "📊" },
{ label: "Sign in", href: "/sign-in", icon: "🔑" },
{ label: "Register", href: "/sign-up", icon: "✎" },
{ label: "Add funds (BTC → USD)", href: "/account/add-funds", icon: "₿" },
{ label: "Onion mirrors", href: "/account/hidden-services", icon: "🧅" },
],
},
{
title: "Commerce",
items: [
{ label: "Dark Bazaar (market)", href: "/market", icon: "📈" },
{ label: "Checkout", href: "/checkout", icon: "🛒" },
{ label: "Wallets & catalog", href: "/wallets", icon: "👛" },
{ label: "Vendor hall", href: "/vendors", icon: "🏪" },
{ label: "Apply as vendor", href: "/vendor/apply", icon: "📋" },
{ label: "Floor analytics", href: "/market/analytics", icon: "📉" },
{ label: "Operations", href: "/market/operations", icon: "⚙️" },
{ label: "Buyer intel", href: "/market/intel", icon: "🔍" },
{ label: "Escrow & trust", href: "/market/trust", icon: "🤝" },
],
},
{
title: "Community",
items: [
{ label: "Forum", href: "/forum", icon: "◆" },
{ label: "Messages", href: "/messages", icon: "💬" },
{ label: "Chatter", href: "/chatter", icon: "📡" },
{ label: "Submit post", href: "/forum/submit", icon: "" },
{ label: "Sanctum", href: "/sanctuary", icon: "🛡️" },
{ label: "Inner circle", href: "/inner-circle", icon: "◎" },
],
},
{
title: "Exchange & barter",
items: [
{ label: "Soul Trade", href: "/exchange", icon: "📰" },
{ label: "Exchange ledger", href: "/exchange/ledger", icon: "📒" },
{ label: "Guidelines", href: "/exchange/guidelines", icon: "📜" },
{ label: "Barter", href: "/barter", icon: "♻️" },
],
},
{
title: "Intel & tools",
items: [
{ label: "Grimoire (vault)", href: "/vault", icon: "🗝️" },
{ label: "Forbidden wiki", href: "/hidden-wiki", icon: "📚" },
{ label: "Darknet atlas", href: "/darknet-atlas", icon: "🗺️" },
{ label: "Shadow syndicate", href: "/syndicate", icon: "🕸️" },
{ label: "Routing", href: "/syndicate/routing", icon: "↗" },
{ label: "Topology", href: "/syndicate/topology", icon: "⬡" },
{ label: "Void crawl (search)", href: "/search", icon: "🔦" },
{ label: "Links", href: "/links", icon: "🔗" },
{ label: "Presswire", href: "/presswire", icon: "📣" },
],
},
{
title: "Extras",
items: [
{ label: "Launch", href: "/launch", icon: "🔥" },
{ label: "Drops", href: "/drops", icon: "🚀" },
{ label: "Drop box", href: "/drop-box", icon: "📦" },
{ label: "Raffle", href: "/raffle", icon: "🎟️" },
{ label: "Awards", href: "/awards", icon: "🏆" },
{ label: "Testimonials", href: "/testimonials", icon: "✨" },
{ label: "Reviews", href: "/reviews", icon: "⭐" },
{ label: "Support", href: "/support", icon: "🆘" },
{ label: "Game", href: "/game", icon: "🎮" },
{ label: "Mixer", href: "/mixer", icon: "🌀" },
{ label: "Webring", href: "/webring", icon: "∞" },
{ label: "Trust", href: "/trust", icon: "⚖️" },
{ label: "Security analysis", href: "/security-analysis", icon: "🔐" },
{ label: "Arb academy", href: "/arb-academy", icon: "📚" },
{ label: "Comparison", href: "/comparison", icon: "⚗️" },
{ label: "Conspiracies", href: "/conspiracies", icon: "🕯️" },
{ label: "Trees", href: "/trees", icon: "🌲" },
{ label: "Easter eggs", href: "/easter-eggs", icon: "🥚" },
{ label: "Red room", href: "/red-room", icon: "🚪" },
],
},
];
export function flattenSiteNavItems(): SiteNavItem[] {
return SITE_NAV_GROUPS.flatMap((g) => g.items);
}

File diff suppressed because it is too large Load Diff

364
onions.txt Normal file
View File

@@ -0,0 +1,364 @@
# =============================================================================
# EldritchWeave / CyberLux — Tor hidden service registry (43 v3 onions)
# =============================================================================
#
# Live .onion hostnames are NOT stored in git. Each service has its own keypair
# under /var/lib/tor/<torDir>/ on the machine running Tor.
#
# To print real URLs on the server:
# sudo bash scripts/list-onion-urls.sh
# sudo bash scripts/export-onion-urls.sh # writes onion-urls.txt at repo root
#
# Each address below uses the form: http://<56-char-v3-onion>.onion
# Open in Tor Browser or Onion Browser — use http:// (not https) for v3 onions.
#
# All services proxy one Next.js app (127.0.0.1:3000) via nginx on 127.0.0.1:80808122.
# =============================================================================
# --- 1
# Site: Hub — main storefront (home)
# torDir: cyberlux
# Nginx: 127.0.0.1:8080
# App path: /
# Onion URL: http://<PASTE_HOSTNAME_FROM_/var/lib/tor/cyberlux/hostname>.onion
# --- 2
# Site: Forum — void aggregate / coven
# torDir: cyberlux_forum
# Nginx: 127.0.0.1:8081
# App path: /forum
# Onion URL: http://<PASTE_HOSTNAME_FROM_/var/lib/tor/cyberlux_forum/hostname>.onion
# --- 3
# Site: Exchange — classifieds / soul trade
# torDir: cyberlux_exchange
# Nginx: 127.0.0.1:8082
# App path: /exchange
# Onion URL: http://<PASTE_HOSTNAME_FROM_/var/lib/tor/cyberlux_exchange/hostname>.onion
# --- 4
# Site: Hidden wiki layer
# torDir: cyberlux_wiki
# Nginx: 127.0.0.1:8083
# App path: /hidden-wiki
# Onion URL: http://<PASTE_HOSTNAME_FROM_/var/lib/tor/cyberlux_wiki/hostname>.onion
# --- 5
# Site: Market — full catalog / Dark Bazaar
# torDir: cyberlux_market
# Nginx: 127.0.0.1:8084
# App path: /market
# Onion URL: http://<PASTE_HOSTNAME_FROM_/var/lib/tor/cyberlux_market/hostname>.onion
# --- 6
# Site: Barter — ash pit barter board
# torDir: cyberlux_barter
# Nginx: 127.0.0.1:8085
# App path: /barter
# Onion URL: http://<PASTE_HOSTNAME_FROM_/var/lib/tor/cyberlux_barter/hostname>.onion
# --- 7
# Site: Chatter — live feed
# torDir: cyberlux_chatter
# Nginx: 127.0.0.1:8086
# App path: /chatter
# Onion URL: http://<PASTE_HOSTNAME_FROM_/var/lib/tor/cyberlux_chatter/hostname>.onion
# --- 8
# Site: Search — void crawler
# torDir: cyberlux_search
# Nginx: 127.0.0.1:8087
# App path: /search
# Onion URL: http://<PASTE_HOSTNAME_FROM_/var/lib/tor/cyberlux_search/hostname>.onion
# --- 9
# Site: Syndicate — shell network
# torDir: cyberlux_syndicate
# Nginx: 127.0.0.1:8088
# App path: /syndicate
# Onion URL: http://<PASTE_HOSTNAME_FROM_/var/lib/tor/cyberlux_syndicate/hostname>.onion
# --- 10
# Site: Arb Academy
# torDir: cyberlux_arb_academy
# Nginx: 127.0.0.1:8089
# App path: /arb-academy
# Onion URL: http://<PASTE_HOSTNAME_FROM_/var/lib/tor/cyberlux_arb_academy/hostname>.onion
# --- 11
# Site: Reviews
# torDir: cyberlux_reviews
# Nginx: 127.0.0.1:8090
# App path: /reviews
# Onion URL: http://<PASTE_HOSTNAME_FROM_/var/lib/tor/cyberlux_reviews/hostname>.onion
# --- 12
# Site: Trust — reputation layer
# torDir: cyberlux_trust
# Nginx: 127.0.0.1:8091
# App path: /trust
# Onion URL: http://<PASTE_HOSTNAME_FROM_/var/lib/tor/cyberlux_trust/hostname>.onion
# --- 13
# Site: Vault / grimoire (incl. network admin on /vault)
# torDir: cyberlux_vault
# Nginx: 127.0.0.1:8092
# App path: /vault
# Onion URL: http://<PASTE_HOSTNAME_FROM_/var/lib/tor/cyberlux_vault/hostname>.onion
# --- 14
# Site: Messages — global / synced comms
# torDir: cyberlux_messages
# Nginx: 127.0.0.1:8093
# App path: /messages
# Onion URL: http://<PASTE_HOSTNAME_FROM_/var/lib/tor/cyberlux_messages/hostname>.onion
# --- 15
# Site: Drop box
# torDir: cyberlux_drop_box
# Nginx: 127.0.0.1:8094
# App path: /drop-box
# Onion URL: http://<PASTE_HOSTNAME_FROM_/var/lib/tor/cyberlux_drop_box/hostname>.onion
# --- 16
# Site: Easter eggs
# torDir: cyberlux_easter_eggs
# Nginx: 127.0.0.1:8095
# App path: /easter-eggs
# Onion URL: http://<PASTE_HOSTNAME_FROM_/var/lib/tor/cyberlux_easter_eggs/hostname>.onion
# --- 17
# Site: Curated links
# torDir: cyberlux_links
# Nginx: 127.0.0.1:8096
# App path: /links
# Onion URL: http://<PASTE_HOSTNAME_FROM_/var/lib/tor/cyberlux_links/hostname>.onion
# --- 18
# Site: Red room
# torDir: cyberlux_red_room
# Nginx: 127.0.0.1:8097
# App path: /red-room
# Onion URL: http://<PASTE_HOSTNAME_FROM_/var/lib/tor/cyberlux_red_room/hostname>.onion
# --- 19
# Site: Drops
# torDir: cyberlux_drops
# Nginx: 127.0.0.1:8098
# App path: /drops
# Onion URL: http://<PASTE_HOSTNAME_FROM_/var/lib/tor/cyberlux_drops/hostname>.onion
# --- 20
# Site: Inner circle
# torDir: cyberlux_inner_circle
# Nginx: 127.0.0.1:8099
# App path: /inner-circle
# Onion URL: http://<PASTE_HOSTNAME_FROM_/var/lib/tor/cyberlux_inner_circle/hostname>.onion
# --- 21
# Site: Comparison tool
# torDir: cyberlux_comparison
# Nginx: 127.0.0.1:8100
# App path: /comparison
# Onion URL: http://<PASTE_HOSTNAME_FROM_/var/lib/tor/cyberlux_comparison/hostname>.onion
# --- 22
# Site: Testimonials
# torDir: cyberlux_testimonials
# Nginx: 127.0.0.1:8101
# App path: /testimonials
# Onion URL: http://<PASTE_HOSTNAME_FROM_/var/lib/tor/cyberlux_testimonials/hostname>.onion
# --- 23
# Site: Wallets — digital passes / wallet UX
# torDir: cyberlux_wallets
# Nginx: 127.0.0.1:8102
# App path: /wallets
# Onion URL: http://<PASTE_HOSTNAME_FROM_/var/lib/tor/cyberlux_wallets/hostname>.onion
# --- 24
# Site: Support desk
# torDir: cyberlux_support
# Nginx: 127.0.0.1:8103
# App path: /support
# Onion URL: http://<PASTE_HOSTNAME_FROM_/var/lib/tor/cyberlux_support/hostname>.onion
# --- 25
# Site: Darknet atlas
# torDir: cyberlux_darknet_atlas
# Nginx: 127.0.0.1:8104
# App path: /darknet-atlas
# Onion URL: http://<PASTE_HOSTNAME_FROM_/var/lib/tor/cyberlux_darknet_atlas/hostname>.onion
# --- 26
# Site: Security analysis
# torDir: cyberlux_security_analysis
# Nginx: 127.0.0.1:8105
# App path: /security-analysis
# Onion URL: http://<PASTE_HOSTNAME_FROM_/var/lib/tor/cyberlux_security_analysis/hostname>.onion
# --- 27
# Site: Mixer
# torDir: cyberlux_mixer
# Nginx: 127.0.0.1:8106
# App path: /mixer
# Onion URL: http://<PASTE_HOSTNAME_FROM_/var/lib/tor/cyberlux_mixer/hostname>.onion
# --- 28
# Site: Secret layer
# torDir: cyberlux_secret_layer
# Nginx: 127.0.0.1:8107
# App path: /secret-layer
# Onion URL: http://<PASTE_HOSTNAME_FROM_/var/lib/tor/cyberlux_secret_layer/hostname>.onion
# --- 29
# Site: Trees
# torDir: cyberlux_trees
# Nginx: 127.0.0.1:8108
# App path: /trees
# Onion URL: http://<PASTE_HOSTNAME_FROM_/var/lib/tor/cyberlux_trees/hostname>.onion
# --- 30
# Site: Presswire / news
# torDir: cyberlux_presswire
# Nginx: 127.0.0.1:8109
# App path: /presswire
# Onion URL: http://<PASTE_HOSTNAME_FROM_/var/lib/tor/cyberlux_presswire/hostname>.onion
# --- 31
# Site: Awards
# torDir: cyberlux_awards
# Nginx: 127.0.0.1:8110
# App path: /awards
# Onion URL: http://<PASTE_HOSTNAME_FROM_/var/lib/tor/cyberlux_awards/hostname>.onion
# --- 32
# Site: Raffle
# torDir: cyberlux_raffle
# Nginx: 127.0.0.1:8111
# App path: /raffle
# Onion URL: http://<PASTE_HOSTNAME_FROM_/var/lib/tor/cyberlux_raffle/hostname>.onion
# --- 33
# Site: Game
# torDir: cyberlux_game
# Nginx: 127.0.0.1:8112
# App path: /game
# Onion URL: http://<PASTE_HOSTNAME_FROM_/var/lib/tor/cyberlux_game/hostname>.onion
# --- 34
# Site: Webring
# torDir: cyberlux_webring
# Nginx: 127.0.0.1:8113
# App path: /webring
# Onion URL: http://<PASTE_HOSTNAME_FROM_/var/lib/tor/cyberlux_webring/hostname>.onion
# --- 35
# Site: Conspiracies
# torDir: cyberlux_conspiracies
# Nginx: 127.0.0.1:8114
# App path: /conspiracies
# Onion URL: http://<PASTE_HOSTNAME_FROM_/var/lib/tor/cyberlux_conspiracies/hostname>.onion
# --- 36
# Site: Sanctuary / sanctum
# torDir: cyberlux_sanctuary
# Nginx: 127.0.0.1:8115
# App path: /sanctuary
# Onion URL: http://<PASTE_HOSTNAME_FROM_/var/lib/tor/cyberlux_sanctuary/hostname>.onion
# --- 37
# Site: Dashboard — user relay / profile
# torDir: cyberlux_dashboard
# Nginx: 127.0.0.1:8116
# App path: /dashboard
# Onion URL: http://<PASTE_HOSTNAME_FROM_/var/lib/tor/cyberlux_dashboard/hostname>.onion
# --- 38
# Site: Checkout — blood pact / cart
# torDir: cyberlux_checkout
# Nginx: 127.0.0.1:8117
# App path: /checkout
# Onion URL: http://<PASTE_HOSTNAME_FROM_/var/lib/tor/cyberlux_checkout/hostname>.onion
# --- 39
# Site: Vendors — vendor hall
# torDir: cyberlux_vendors
# Nginx: 127.0.0.1:8118
# App path: /vendors
# Onion URL: http://<PASTE_HOSTNAME_FROM_/var/lib/tor/cyberlux_vendors/hostname>.onion
# --- 40
# Site: Sign in
# torDir: cyberlux_sign_in
# Nginx: 127.0.0.1:8119
# App path: /sign-in
# Onion URL: http://<PASTE_HOSTNAME_FROM_/var/lib/tor/cyberlux_sign_in/hostname>.onion
# --- 41
# Site: Sign up / register
# torDir: cyberlux_sign_up
# Nginx: 127.0.0.1:8120
# App path: /sign-up
# Onion URL: http://<PASTE_HOSTNAME_FROM_/var/lib/tor/cyberlux_sign_up/hostname>.onion
# --- 42
# Site: Account — add funds, mirror map, hidden services
# torDir: cyberlux_account
# Nginx: 127.0.0.1:8121
# App path: /account
# Onion URL: http://<PASTE_HOSTNAME_FROM_/var/lib/tor/cyberlux_account/hostname>.onion
# --- 43
# Site: Shadow nodes / short links ( /w/* syndicate shell )
# torDir: cyberlux_w
# Nginx: 127.0.0.1:8122
# App path: /w (and related routes)
# Onion URL: http://<PASTE_HOSTNAME_FROM_/var/lib/tor/cyberlux_w/hostname>.onion
# =============================================================================
# Quick index (torDir → nginx port) — same order as scripts/onion-nodes.json
# =============================================================================
# cyberlux 8080
# cyberlux_forum 8081
# cyberlux_exchange 8082
# cyberlux_wiki 8083
# cyberlux_market 8084
# cyberlux_barter 8085
# cyberlux_chatter 8086
# cyberlux_search 8087
# cyberlux_syndicate 8088
# cyberlux_arb_academy 8089
# cyberlux_reviews 8090
# cyberlux_trust 8091
# cyberlux_vault 8092
# cyberlux_messages 8093
# cyberlux_drop_box 8094
# cyberlux_easter_eggs 8095
# cyberlux_links 8096
# cyberlux_red_room 8097
# cyberlux_drops 8098
# cyberlux_inner_circle 8099
# cyberlux_comparison 8100
# cyberlux_testimonials 8101
# cyberlux_wallets 8102
# cyberlux_support 8103
# cyberlux_darknet_atlas 8104
# cyberlux_security_analysis 8105
# cyberlux_mixer 8106
# cyberlux_secret_layer 8107
# cyberlux_trees 8108
# cyberlux_presswire 8109
# cyberlux_awards 8110
# cyberlux_raffle 8111
# cyberlux_game 8112
# cyberlux_webring 8113
# cyberlux_conspiracies 8114
# cyberlux_sanctuary 8115
# cyberlux_dashboard 8116
# cyberlux_checkout 8117
# cyberlux_vendors 8118
# cyberlux_sign_in 8119
# cyberlux_sign_up 8120
# cyberlux_account 8121
# cyberlux_w 8122

View File

@@ -7,9 +7,10 @@
"verify": "node scripts/verify.cjs", "verify": "node scripts/verify.cjs",
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start -H 127.0.0.1 -p 3000",
"start:onion": "next start -H 127.0.0.1 -p 3000", "start:onion": "next start -H 127.0.0.1 -p 3000",
"health:stack": "bash scripts/health-check-stack.sh", "health:stack": "bash scripts/health-check-stack.sh",
"diagnose:onion": "bash scripts/diagnose-onion-stack.sh",
"onions:status": "node scripts/onion-status.cjs", "onions:status": "node scripts/onion-status.cjs",
"onions:list": "bash scripts/list-onion-urls.sh", "onions:list": "bash scripts/list-onion-urls.sh",
"install:systemd": "bash -c 'echo Run: sudo CYBERLUX_USER=$USER bash scripts/install-systemd.sh'", "install:systemd": "bash -c 'echo Run: sudo CYBERLUX_USER=$USER bash scripts/install-systemd.sh'",

View File

@@ -6,65 +6,15 @@ import {
type CyberluxEntry, type CyberluxEntry,
} from "@/lib/cyberluxEntry"; } from "@/lib/cyberluxEntry";
import { DEDICATED_ROOT } from "@/lib/onionRoutes.generated"; import { DEDICATED_ROOT } from "@/lib/onionRoutes.generated";
import { isOnionCrossNavPath } from "@/lib/cyberluxCrossNav";
/** Set by nginx per Tor vhost: `proxy_set_header X-Cyberlux-Node <node>;` */ /** Set by nginx per Tor vhost: `proxy_set_header X-Cyberlux-Node <node>;` */
function entryFromRequest(request: NextRequest): CyberluxEntry { function entryFromRequest(request: NextRequest): CyberluxEntry {
return parseCyberluxEntry(request.headers.get("x-cyberlux-node")); return parseCyberluxEntry(request.headers.get("x-cyberlux-node"));
} }
/**
* Paths that must not be prefixed when using a dedicated entry onion.
* Keep in sync with top-level `app/<segment>/` routes.
*/
const CROSS_NAV_PREFIXES = [
"/account",
"/api",
"/arb-academy",
"/awards",
"/barter",
"/chatter",
"/checkout",
"/comparison",
"/conspiracies",
"/darknet-atlas",
"/dashboard",
"/drop-box",
"/drops",
"/easter-eggs",
"/exchange",
"/forum",
"/game",
"/hidden-wiki",
"/inner-circle",
"/links",
"/market",
"/messages",
"/mixer",
"/presswire",
"/raffle",
"/red-room",
"/reviews",
"/sanctuary",
"/search",
"/secret-layer",
"/security-analysis",
"/sign-in",
"/sign-up",
"/support",
"/syndicate",
"/testimonials",
"/trees",
"/trust",
"/vault",
"/vendor",
"/vendors",
"/wallets",
"/webring",
"/w/",
] as const;
function isCrossNavPath(pathname: string): boolean { function isCrossNavPath(pathname: string): boolean {
return CROSS_NAV_PREFIXES.some((p) => pathname.startsWith(p)); return isOnionCrossNavPath(pathname);
} }
function withEntryCookie(res: NextResponse, request: NextRequest): NextResponse { function withEntryCookie(res: NextResponse, request: NextRequest): NextResponse {

View File

@@ -0,0 +1,60 @@
#!/usr/bin/env bash
# Quick local checks for Tor → nginx → Next. Does not need sudo.
# If .onion URLs fail in Tor Browser but this script is OK, export fresh URLs:
# sudo bash scripts/export-onion-urls.sh
# and use http:// (not https://) in Tor Browser.
set -euo pipefail
REPO="$(cd "$(dirname "$0")/.." && pwd)"
cd "${REPO}"
echo ""
echo "━━ CyberLux onion stack (local) ━━"
echo ""
http_code() {
curl -g -sS -o /dev/null -w "%{http_code}" --connect-timeout 3 --max-time 8 "$1" 2>/dev/null || echo "000"
}
c300="$(http_code "http://127.0.0.1:3000/")"
c8080="$(http_code "http://127.0.0.1:8080/")"
echo " Next.js (upstream) 127.0.0.1:3000 → HTTP ${c300}"
echo " nginx hub (Tor target) 127.0.0.1:8080 → HTTP ${c8080}"
if [[ "${c300}" =~ ^(200|301|302|304)$ ]]; then
echo " Next.js: OK"
else
echo " Next.js: FAIL — onions will 502 until you run: npm run start:onion or systemctl start cyberlux.service"
fi
if [[ "${c8080}" =~ ^(200|301|302|304)$ ]]; then
echo " Hub vhost: OK"
else
echo " Hub vhost: FAIL — check: systemctl status nginx"
fi
if command -v systemctl >/dev/null 2>&1; then
echo ""
echo " systemd:"
systemctl is-active tor@default 2>/dev/null | sed 's/^/ tor@default: /' || echo " tor@default: (unknown)"
systemctl is-active nginx 2>/dev/null | sed 's/^/ nginx: /' || echo " nginx: (unknown)"
if systemctl list-unit-files cyberlux.service &>/dev/null; then
systemctl is-active cyberlux.service 2>/dev/null | sed 's/^/ cyberlux.service: /' || true
else
echo " cyberlux.service: not installed (optional: sudo CYBERLUX_USER=\$USER bash scripts/install-systemd.sh)"
fi
fi
echo ""
if [[ -f "${REPO}/onion-urls.txt" ]]; then
echo " onion-urls.txt: present (first lines):"
grep -vE '^#|^$' "${REPO}/onion-urls.txt" 2>/dev/null | head -6 | sed 's/^/ /' || true
else
echo " onion-urls.txt: missing — run after Tor is up:"
echo " sudo bash scripts/export-onion-urls.sh"
fi
echo ""
echo " Full port + hostname table: node scripts/onion-status.cjs"
echo ""

View File

@@ -66,6 +66,8 @@ read_host() {
[[ "${missing}" -gt 0 ]] && echo "# Services pending: ${missing} (start Tor and wait ~30s)" [[ "${missing}" -gt 0 ]] && echo "# Services pending: ${missing} (start Tor and wait ~30s)"
} > "${OUT}" } > "${OUT}"
chmod a+r "${OUT}" 2>/dev/null || true
echo "[*] Wrote ${OUT}" echo "[*] Wrote ${OUT}"
echo "[*] Services resolved: ${ready} / $((ready + missing))" echo "[*] Services resolved: ${ready} / $((ready + missing))"
[[ "${missing}" -gt 0 ]] && echo "[!] ${missing} hostname(s) not yet available — run again after Tor fully starts." [[ "${missing}" -gt 0 ]] && echo "[!] ${missing} hostname(s) not yet available — run again after Tor fully starts."

View File

@@ -41,6 +41,7 @@ server {
proxy_set_header X-Forwarded-Proto http; proxy_set_header X-Forwarded-Proto http;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade"; proxy_set_header Connection "upgrade";
proxy_set_header X-Cyberlux-Node ${h};
} }
location /api/ { location /api/ {
proxy_pass http://127.0.0.1:3000; proxy_pass http://127.0.0.1:3000;
@@ -50,6 +51,7 @@ server {
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto http; proxy_set_header X-Forwarded-Proto http;
proxy_set_header X-Cyberlux-Node ${h};
} }
location /forum { location /forum {
proxy_pass http://127.0.0.1:3000; proxy_pass http://127.0.0.1:3000;
@@ -59,6 +61,7 @@ server {
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto http; proxy_set_header X-Forwarded-Proto http;
proxy_set_header X-Cyberlux-Node ${h};
} }
location /exchange { location /exchange {
proxy_pass http://127.0.0.1:3000; proxy_pass http://127.0.0.1:3000;
@@ -68,6 +71,7 @@ server {
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto http; proxy_set_header X-Forwarded-Proto http;
proxy_set_header X-Cyberlux-Node ${h};
} }
location / { location / {
@@ -109,6 +113,7 @@ server {
proxy_set_header X-Forwarded-Proto http; proxy_set_header X-Forwarded-Proto http;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade"; proxy_set_header Connection "upgrade";
proxy_set_header X-Cyberlux-Node "wiki";
} }
location /api/ { location /api/ {
proxy_pass http://127.0.0.1:3000; proxy_pass http://127.0.0.1:3000;
@@ -118,6 +123,7 @@ server {
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto http; proxy_set_header X-Forwarded-Proto http;
proxy_set_header X-Cyberlux-Node "wiki";
} }
location /forum { location /forum {
proxy_pass http://127.0.0.1:3000; proxy_pass http://127.0.0.1:3000;
@@ -127,6 +133,7 @@ server {
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto http; proxy_set_header X-Forwarded-Proto http;
proxy_set_header X-Cyberlux-Node "wiki";
} }
location /exchange { location /exchange {
proxy_pass http://127.0.0.1:3000; proxy_pass http://127.0.0.1:3000;
@@ -136,6 +143,7 @@ server {
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto http; proxy_set_header X-Forwarded-Proto http;
proxy_set_header X-Cyberlux-Node "wiki";
} }
location / { location / {
@@ -177,6 +185,7 @@ server {
proxy_set_header X-Forwarded-Proto http; proxy_set_header X-Forwarded-Proto http;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade"; proxy_set_header Connection "upgrade";
proxy_set_header X-Cyberlux-Node "w";
} }
location /api/ { location /api/ {
proxy_pass http://127.0.0.1:3000; proxy_pass http://127.0.0.1:3000;
@@ -186,6 +195,7 @@ server {
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto http; proxy_set_header X-Forwarded-Proto http;
proxy_set_header X-Cyberlux-Node "w";
} }
location /forum { location /forum {
proxy_pass http://127.0.0.1:3000; proxy_pass http://127.0.0.1:3000;
@@ -195,6 +205,7 @@ server {
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto http; proxy_set_header X-Forwarded-Proto http;
proxy_set_header X-Cyberlux-Node "w";
} }
location /exchange { location /exchange {
proxy_pass http://127.0.0.1:3000; proxy_pass http://127.0.0.1:3000;
@@ -204,6 +215,7 @@ server {
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto http; proxy_set_header X-Forwarded-Proto http;
proxy_set_header X-Cyberlux-Node "w";
} }
location / { location / {

0
scripts/health-check-stack.sh Normal file → Executable file
View File

View File

@@ -79,7 +79,7 @@ EnvironmentFile=-${ENV_FILE}
ExecStartPre=${NODE_BIN} ${CYBERLUX_REPO}/scripts/generate-onion-config.cjs ExecStartPre=${NODE_BIN} ${CYBERLUX_REPO}/scripts/generate-onion-config.cjs
ExecStart=${NPM_BIN} run start:onion ExecStart=${NPM_BIN} run start:onion
Restart=on-failure Restart=always
RestartSec=5 RestartSec=5
# Avoid thrashing if Tor/nginx are still starting # Avoid thrashing if Tor/nginx are still starting
StartLimitIntervalSec=120 StartLimitIntervalSec=120

View File

@@ -95,6 +95,21 @@ elif ! tor_is_active; then
start_tor start_tor
fi fi
# Hidden services proxy to nginx → Next on 127.0.0.1:3000. If Next is down, every .onion returns 502.
if command -v curl >/dev/null 2>&1; then
up="$(curl -g -sS -o /dev/null -w "%{http_code}" --connect-timeout 2 --max-time 5 "http://127.0.0.1:3000/" 2>/dev/null)" || up="000"
if [[ ! "${up}" =~ ^(200|301|302|304)$ ]]; then
echo "[!] Next.js is not serving on 127.0.0.1:3000 (HTTP ${up}) — onion URLs will fail until it is running." >&2
echo " Start: cd ${REPO} && npm run start:onion or: sudo systemctl start cyberlux.service" >&2
echo " Install service: sudo CYBERLUX_USER=\${SUDO_USER:-\$USER} bash ${REPO}/scripts/install-systemd.sh" >&2
fi
fi
# Refresh repo-root onion-urls.txt while we have root (hostname dirs are 0700 debian-tor).
if [[ -f "${REPO}/scripts/export-onion-urls.sh" ]]; then
bash "${REPO}/scripts/export-onion-urls.sh" || true
fi
if [[ "${CYBERLUX_INSTALL_QUIET:-}" == "1" ]]; then if [[ "${CYBERLUX_INSTALL_QUIET:-}" == "1" ]]; then
exit 0 exit 0
fi fi

View File

@@ -30,18 +30,30 @@ echo ""
echo "CyberLux .onion URLs (http:// only — open in Tor Browser)" echo "CyberLux .onion URLs (http:// only — open in Tor Browser)"
echo "────────────────────────────────────────────────────────────" echo "────────────────────────────────────────────────────────────"
ready=0
while IFS= read -r d || [[ -n "${d}" ]]; do while IFS= read -r d || [[ -n "${d}" ]]; do
[[ -z "${d}" ]] && continue [[ -z "${d}" ]] && continue
f="/var/lib/tor/${d}/hostname" f="/var/lib/tor/${d}/hostname"
host="$(read_host "${f}")" host="$(read_host "${f}")"
if [[ -n "${host}" ]]; then if [[ -n "${host}" ]]; then
printf '%-28s http://%s\n' "${d}" "${host}" printf '%-28s http://%s\n' "${d}" "${host}"
((ready++)) || true
else else
printf '%-28s (no hostname yet — %s)\n' "${d}" "${f}" printf '%-28s (no hostname yet — %s)\n' "${d}" "${f}"
fi fi
done < "${TOR_DIRS}" done < "${TOR_DIRS}"
echo "────────────────────────────────────────────────────────────" echo "────────────────────────────────────────────────────────────"
echo "" if [[ "${ready}" -eq 0 ]] && [[ -f "${REPO}/onion-urls.txt" ]]; then
echo "If lines show (no hostname yet): wait for Tor, or sudo ls -la /var/lib/tor/" echo ""
echo "Could not read /var/lib/tor (permission). Last exported list:"
echo "────────────────────────────────────────────────────────────"
grep -vE '^#|^$' "${REPO}/onion-urls.txt" 2>/dev/null | head -120 || true
echo ""
echo "Refresh: sudo bash ${REPO}/scripts/export-onion-urls.sh"
elif [[ "${ready}" -eq 0 ]]; then
echo ""
echo "No hostnames resolved. Ensure Tor is running and keys exist under /var/lib/tor/,"
echo "or run: sudo bash ${REPO}/scripts/export-onion-urls.sh"
fi
echo "" echo ""

View File

@@ -6,7 +6,8 @@
* node scripts/onion-status.cjs * node scripts/onion-status.cjs
* npm run onions:status * npm run onions:status
* *
* Reading /var/lib/tor/*/hostname usually requires sudo: * Reading hostname files under /var/lib/tor (one directory per service) usually
* requires sudo:
* sudo node scripts/onion-status.cjs * sudo node scripts/onion-status.cjs
*/ */
@@ -20,21 +21,46 @@ const REPO = path.join(__dirname, "..");
const jsonPath = path.join(__dirname, "onion-nodes.json"); const jsonPath = path.join(__dirname, "onion-nodes.json");
const data = JSON.parse(fs.readFileSync(jsonPath, "utf8")); const data = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
/** Populated by export-onion-urls.sh — readable when /var/lib/tor is root-only. */
function readOnionFromExportFile(torDir) {
const exportPath = path.join(REPO, "onion-urls.txt");
let raw;
try {
raw = fs.readFileSync(exportPath, "utf8");
} catch {
return "";
}
const line = raw.split("\n").find((l) => l.trimStart().startsWith(torDir));
if (!line) return "";
const m = line.match(/https?:\/\/([a-z2-7]{56}\.onion)\b/i);
return m ? m[1] : "";
}
function readOnionHost(torDir) { function readOnionHost(torDir) {
const f = path.join("/var/lib/tor", torDir, "hostname"); const f = path.join("/var/lib/tor", torDir, "hostname");
try { try {
return fs.readFileSync(f, "utf8").trim(); return fs.readFileSync(f, "utf8").trim();
} catch (e) { } catch (e) {
if (e.code === "EACCES" || e.code === "EPERM") { if (e.code === "EACCES" || e.code === "EPERM") {
return "(hostname file exists but not readable — try: sudo node scripts/onion-status.cjs)"; const fromFile = readOnionFromExportFile(torDir);
if (fromFile) return `${fromFile} (from onion-urls.txt)`;
return "(hostname not readable — run: sudo bash scripts/export-onion-urls.sh, then retry)";
} }
if (e.code === "ENOENT") { if (e.code === "ENOENT") {
const fromFile = readOnionFromExportFile(torDir);
if (fromFile) return `${fromFile} (from onion-urls.txt)`;
return "(no hostname yet — is Tor running?)"; return "(no hostname yet — is Tor running?)";
} }
return `(${e.message})`; return `(${e.message})`;
} }
} }
function onionHttpUrl(onionField) {
const m = String(onionField).match(/([a-z2-7]{56}\.onion)/i);
if (m) return `http://${m[1]}`;
return onionField;
}
function checkLocalPort(port) { function checkLocalPort(port) {
return new Promise((resolve) => { return new Promise((resolve) => {
const req = http.request( const req = http.request(
@@ -77,14 +103,14 @@ async function main() {
} }
console.log(""); console.log("");
console.log("CyberLux — onion URLs (from /var/lib/tor/*/hostname) + loopback health"); console.log("CyberLux — onion URLs (from /var/lib/tor/<service>/hostname) + loopback health");
console.log("─".repeat(100)); console.log("─".repeat(100));
let bad = 0; let bad = 0;
for (const r of rows) { for (const r of rows) {
const url = const url =
r.onion.startsWith("(") || r.onion.includes("not readable") r.onion.startsWith("(") || r.onion.includes("not readable")
? r.onion ? r.onion
: `http://${r.onion}`; : onionHttpUrl(r.onion);
const local = `http://127.0.0.1:${r.port}/`; const local = `http://127.0.0.1:${r.port}/`;
const status = const status =
r.httpCode === 0 ? "DOWN/timeout" : `HTTP ${r.httpCode}`; r.httpCode === 0 ? "DOWN/timeout" : `HTTP ${r.httpCode}`;

View File

@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# Start CyberLux onion backend if 127.0.0.1:3000 is not already occupied.
# Used by the user systemd unit and @reboot cron fallback.
set -euo pipefail
REPO="/home/drjones/cyberlux"
LOG="${REPO}/logs/onion-runtime.log"
mkdir -p "${REPO}/logs"
if command -v ss >/dev/null 2>&1 && ss -ltn 'sport = :3000' | grep -q '127.0.0.1:3000'; then
echo "$(date -Is) cyberlux already listening on 127.0.0.1:3000" >>"${LOG}"
exit 0
fi
cd "${REPO}"
node scripts/generate-onion-config.cjs >>"${LOG}" 2>&1
exec npm run start:onion >>"${LOG}" 2>&1

View File

@@ -20,8 +20,9 @@ try {
run("TypeScript (tsc --noEmit)", "npx tsc --noEmit"); run("TypeScript (tsc --noEmit)", "npx tsc --noEmit");
run( run(
"bash syntax (start.sh, install, backup, restore, systemd, health)", "bash syntax (start.sh, install, backup, restore, systemd, health)",
"bash -n start.sh && bash -n scripts/install-tor-onion.sh && bash -n scripts/install-systemd.sh && bash -n scripts/health-check-stack.sh && bash -n scripts/list-onion-urls.sh && bash -n scripts/backup-onion-keys.sh && bash -n scripts/restore-onion-keys.sh", "bash -n start.sh && bash -n scripts/install-tor-onion.sh && bash -n scripts/install-systemd.sh && bash -n scripts/health-check-stack.sh && bash -n scripts/list-onion-urls.sh && bash -n scripts/export-onion-urls.sh && bash -n scripts/diagnose-onion-stack.sh && bash -n scripts/backup-onion-keys.sh && bash -n scripts/restore-onion-keys.sh",
); );
run("node syntax (onion-status.cjs)", "node --check scripts/onion-status.cjs");
run("Next.js production build", "npm run build"); run("Next.js production build", "npm run build");
console.log("\n✓ verify: all checks passed.\n"); console.log("\n✓ verify: all checks passed.\n");
} catch { } catch {

View File

@@ -242,6 +242,11 @@ else
sudo bash "${BACKUP_SCRIPT}" || echo "[!] Onion key backup refresh failed." sudo bash "${BACKUP_SCRIPT}" || echo "[!] Onion key backup refresh failed."
fi fi
if [[ -f "${REPO}/scripts/export-onion-urls.sh" ]]; then
echo "[*] Exporting live .onion list to onion-urls.txt (readable without /var/lib/tor access)…"
sudo bash "${REPO}/scripts/export-onion-urls.sh" || echo "[!] export-onion-urls.sh failed (URLs still in Tor; re-run with sudo if needed)."
fi
print_onion_banner print_onion_banner
fi fi