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:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -42,3 +42,7 @@ next-env.d.ts
|
||||
|
||||
# runtime-generated onion URL file (populated by scripts/export-onion-urls.sh)
|
||||
onion-urls.txt
|
||||
|
||||
# runtime data
|
||||
.messages.json
|
||||
/logs/
|
||||
|
||||
28
DEPLOY.md
28
DEPLOY.md
@@ -74,6 +74,34 @@ sudo node scripts/onion-status.cjs
|
||||
3. `sudo bash scripts/install-tor-onion.sh`
|
||||
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
|
||||
|
||||
```bash
|
||||
|
||||
59
app/account/page.tsx
Normal file
59
app/account/page.tsx
Normal 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
79
app/api/messages/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,22 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #f0f0f0;
|
||||
--primary: #8b00ff;
|
||||
--secondary: #00e0ff;
|
||||
--background: #030008;
|
||||
--foreground: #e6ffed;
|
||||
--primary: #00ff66;
|
||||
--secondary: #9000ff;
|
||||
--accent: #ff0055;
|
||||
--muted: #1a1a1a;
|
||||
--card: rgba(20, 20, 30, 0.8);
|
||||
--glass: rgba(255, 255, 255, 0.05);
|
||||
--glass-strong: rgba(255, 255, 255, 0.075);
|
||||
--ring: rgba(0, 255, 255, 0.35);
|
||||
--shadow-soft: 0 18px 60px rgba(0, 0, 0, 0.55);
|
||||
--shadow-glass: 0 10px 40px rgba(0, 0, 0, 0.35);
|
||||
--neon-cyan: #00ffff;
|
||||
--neon-purple: #9d00ff;
|
||||
--neon-pink: #ff00ff;
|
||||
--neon-green: #00ff9d;
|
||||
--muted: #0d1210;
|
||||
--card: rgba(5, 10, 5, 0.85);
|
||||
--glass: rgba(0, 255, 100, 0.04);
|
||||
--glass-strong: rgba(144, 0, 255, 0.08);
|
||||
--ring: rgba(0, 255, 102, 0.35);
|
||||
--shadow-soft: 0 18px 60px rgba(0, 255, 102, 0.15);
|
||||
--shadow-glass: 0 10px 40px rgba(144, 0, 255, 0.15);
|
||||
--neon-cyan: #00ff66;
|
||||
--neon-purple: #9000ff;
|
||||
--neon-pink: #ff0066;
|
||||
--neon-green: #b3ff00;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
@@ -65,11 +65,11 @@ body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-image:
|
||||
radial-gradient(1200px 700px at 15% 20%, rgba(157, 0, 255, 0.18) 0%, transparent 55%),
|
||||
radial-gradient(900px 600px at 85% 65%, rgba(0, 255, 255, 0.16) 0%, transparent 55%),
|
||||
radial-gradient(700px 450px at 65% 15%, rgba(255, 0, 255, 0.10) 0%, transparent 50%),
|
||||
radial-gradient(900px 700px at 30% 85%, rgba(0, 255, 157, 0.10) 0%, transparent 55%),
|
||||
linear-gradient(180deg, rgba(255,255,255,0.03), transparent 55%);
|
||||
radial-gradient(1200px 700px at 15% 20%, rgba(144, 0, 255, 0.18) 0%, transparent 55%),
|
||||
radial-gradient(900px 600px at 85% 65%, rgba(0, 255, 102, 0.14) 0%, transparent 55%),
|
||||
radial-gradient(700px 450px at 65% 15%, rgba(255, 0, 102, 0.10) 0%, transparent 50%),
|
||||
radial-gradient(900px 700px at 30% 85%, rgba(179, 255, 0, 0.12) 0%, transparent 55%),
|
||||
linear-gradient(180deg, rgba(0, 255, 102, 0.03), transparent 55%);
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ const ENTRIES: { cat: string; items: WikiItem[] }[] = [
|
||||
items: [
|
||||
{
|
||||
title: "Launch / deploy notes",
|
||||
note: "Tor + nginx operator entry.",
|
||||
note: "Tor + nginx + verify script — operator entry for every .onion on this host.",
|
||||
href: "/launch",
|
||||
external: false,
|
||||
},
|
||||
@@ -227,6 +227,12 @@ export default function HiddenWikiPage() {
|
||||
>
|
||||
All mirrors
|
||||
</Link>
|
||||
<Link
|
||||
href="/launch"
|
||||
className="border border-[#2a3444] bg-[#0f141c] px-3 py-1.5 text-[#94a8b8] hover:border-[#3d5a80]/50"
|
||||
>
|
||||
Launch
|
||||
</Link>
|
||||
<Link
|
||||
href="/syndicate"
|
||||
className="border border-[#2a3444] bg-[#0f141c] px-3 py-1.5 text-[#94a8b8] hover:border-[#3d5a80]/50"
|
||||
|
||||
@@ -18,10 +18,10 @@ const rajdhani = Rajdhani({
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
default: "CyberLux",
|
||||
template: "%s · CyberLux",
|
||||
default: "EldritchWeave",
|
||||
template: "%s · EldritchWeave",
|
||||
},
|
||||
description: "Curated storefront, catalog, forums, and Tor companion rails.",
|
||||
description: "Curated apothecary, grimoires, covenants, and shadow rails.",
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import ThemedLayout from "@/components/layouts/ThemedLayout";
|
||||
import { LINK_GARDEN_INTERNAL } from "@/lib/cyberluxSitemap";
|
||||
|
||||
type Resource = {
|
||||
name: string;
|
||||
@@ -89,19 +90,6 @@ const RESOURCES: Resource[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const INTERNAL: { name: string; description: string; href: string; icon: string }[] = [
|
||||
{ name: "Darknet Atlas", description: "Analyst taxonomy of underground ecosystems — every row links to a real CyberLux surface.", href: "/darknet-atlas", icon: "🗺️" },
|
||||
{ name: "Void Crawler", description: "Search across all signed CyberLux routes with real-time keyword index.", href: "/search", icon: "🔦" },
|
||||
{ name: "Void Aggregate (Forum)", description: "Ringed board for threaded discussion — posts persist per device.", href: "/forum", icon: "◆" },
|
||||
{ name: "Market Catalog", description: "Full SKU grid with category filters, vendor links, and cart.", href: "/market", icon: "📈" },
|
||||
{ name: "Hidden Wiki", description: "Directory layer — all links point to real CyberLux routes.", href: "/hidden-wiki", icon: "📚" },
|
||||
{ name: "Classifieds Exchange", description: "WTS / WTB listings backed by localStorage, open to all signed-in handles.", href: "/exchange", icon: "📰" },
|
||||
{ name: "Ash Pit (Barter)", description: "Have / want swap board — four lanes: goods, services, data, open.", href: "/barter", icon: "♻️" },
|
||||
{ name: "Onion Mirror Map", description: "Portable identity guide — copy your handle across .onion hostnames.", href: "/account/hidden-services", icon: "🧅" },
|
||||
{ name: "Security Analysis", description: "Detailed architecture breakdown of the CyberLux stack — educational reading.", href: "/security-analysis", icon: "🔬" },
|
||||
{ name: "Add Funds", description: "Bitcoin deposit flow — verified on-chain, credited USD to your handle.", href: "/account/add-funds", icon: "₿" },
|
||||
];
|
||||
|
||||
const TRUST_LABELS: Record<Resource["trust"], { label: string; color: string }> = {
|
||||
internal: { label: "Internal", color: "text-neon-green" },
|
||||
clearnet: { label: "Clearnet", color: "text-neon-cyan" },
|
||||
@@ -185,7 +173,7 @@ export default function LinksPage() {
|
||||
<section className="container mx-auto max-w-6xl px-4 py-16">
|
||||
<h2 className="mb-8 font-orbitron text-3xl font-bold">INTERNAL ROUTES</h2>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{INTERNAL.map((item) => (
|
||||
{LINK_GARDEN_INTERNAL.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
import { Suspense } from "react";
|
||||
import MarketSiteChrome from "@/components/market/MarketSiteChrome";
|
||||
|
||||
export default function MarketLayout({ children }: { children: React.ReactNode }) {
|
||||
return <MarketSiteChrome>{children}</MarketSiteChrome>;
|
||||
return (
|
||||
<MarketSiteChrome>
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="py-16 text-center font-mono text-sm text-[#00ff41]/45">Loading catalog…</div>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</Suspense>
|
||||
</MarketSiteChrome>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { ProductSocialBlock } from "@/components/shop/ProductSocialBlock";
|
||||
import { useCart } from "@/contexts/CartContext";
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
type ShopCurrency,
|
||||
type ShopProduct,
|
||||
} from "@/lib/shopCatalog";
|
||||
import { estimateUsdForCryptoAmount } from "@/lib/shopFx";
|
||||
import { getProductSocialMetrics } from "@/lib/shopProductSocial";
|
||||
import { SHOP_SELLER_COUNT, getSellerById } from "@/lib/shopSellers";
|
||||
|
||||
@@ -31,56 +33,129 @@ const SYM: Record<ShopCurrency, string> = {
|
||||
XMR: "⏣",
|
||||
};
|
||||
|
||||
type SortKey = "relevance" | "price-asc" | "price-desc" | "name";
|
||||
|
||||
export default function MarketPage() {
|
||||
const sp = useSearchParams();
|
||||
const router = useRouter();
|
||||
const initialCat = sp.get("category") ?? "";
|
||||
const [q, setQ] = useState("");
|
||||
const [category, setCategory] = useState(initialCat);
|
||||
const [sort, setSort] = useState<SortKey>("relevance");
|
||||
const [currencyFilter, setCurrencyFilter] = useState<ShopCurrency | "">("");
|
||||
const [btcUsd, setBtcUsd] = useState<number | null>(null);
|
||||
|
||||
const facet = useMemo(() => shopCategoryCounts(), []);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
try {
|
||||
const res = await fetch(
|
||||
"https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd",
|
||||
{ cache: "no-store" },
|
||||
);
|
||||
const j = (await res.json()) as { bitcoin?: { usd?: number } };
|
||||
if (!cancelled && j.bitcoin?.usd && Number.isFinite(j.bitcoin.usd)) setBtcUsd(j.bitcoin.usd);
|
||||
} catch {
|
||||
if (!cancelled) setBtcUsd(96000);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const spotBtc = btcUsd ?? 96000;
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const ql = q.trim().toLowerCase();
|
||||
const tokens = ql.split(/\s+/).filter(Boolean);
|
||||
return SHOP_PRODUCTS.filter((p) => {
|
||||
if (category && p.category !== category) return false;
|
||||
if (currencyFilter && p.currency !== currencyFilter) return false;
|
||||
if (tokens.length === 0) return true;
|
||||
const seller = getSellerById(p.sellerId);
|
||||
const sellerHay = seller ? `${seller.handle} ${seller.bio} ${seller.specialty}` : "";
|
||||
const hay = `${p.name} ${p.description} ${p.category} ${p.id} ${sellerHay}`.toLowerCase();
|
||||
return tokens.every((t) => hay.includes(t));
|
||||
});
|
||||
}, [q, category]);
|
||||
}, [q, category, currencyFilter]);
|
||||
|
||||
const sorted = useMemo(() => {
|
||||
const list = [...filtered];
|
||||
const unitUsd = (p: ShopProduct) => estimateUsdForCryptoAmount(p.price, p.currency, spotBtc);
|
||||
switch (sort) {
|
||||
case "price-asc":
|
||||
return list.sort((a, b) => unitUsd(a) - unitUsd(b));
|
||||
case "price-desc":
|
||||
return list.sort((a, b) => unitUsd(b) - unitUsd(a));
|
||||
case "name":
|
||||
return list.sort((a, b) => a.name.localeCompare(b.name));
|
||||
default:
|
||||
return list;
|
||||
}
|
||||
}, [filtered, sort, spotBtc]);
|
||||
|
||||
return (
|
||||
<div className="font-mono text-[#00ff41]">
|
||||
<section className="mb-8 border-b border-[#00ff41]/20 pb-6">
|
||||
<section className="mb-6 border-b border-[#00ff41]/20 pb-4">
|
||||
<p className="text-[10px] uppercase tracking-[0.4em] text-[#00ff41]/50">Layer 1 · live floor</p>
|
||||
<p className="mt-2 max-w-3xl text-sm text-[#00ff41]/65">
|
||||
<strong className="text-[#7af598]">{SHOP_PRODUCT_COUNT}</strong> SKUs ·{" "}
|
||||
<Link href="/vendors" className="font-bold text-[#9dffc4] underline hover:text-white">
|
||||
{SHOP_SELLER_COUNT} vendor stalls
|
||||
</Link>
|
||||
— per-listing reviews, velocity, and cart handoff into checkout (USD estimate from spot). Use the left rail for ops, trust, and intel layers.
|
||||
. Every SKU has a detail page with qty, cart, checkout, or instant USD balance payment. Spot:{" "}
|
||||
<span className="text-[#7af598]">${spotBtc.toLocaleString(undefined, { maximumFractionDigits: 0 })}/BTC</span>.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<div className="mb-6 flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div className="mb-4 flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div className="flex-1">
|
||||
<label className="text-[10px] uppercase tracking-widest text-[#00ff41]/50">Search catalog</label>
|
||||
<input
|
||||
value={q}
|
||||
onChange={(e) => setQ(e.target.value)}
|
||||
placeholder="tokens match title, description, category…"
|
||||
className="mt-1 w-full max-w-xl border-2 border-[#00ff41]/30 bg-black/80 px-4 py-3 text-sm text-[#c8ffd8] placeholder:text-[#00ff41]/25 focus:border-[#00ff41] focus:outline-none"
|
||||
placeholder="title, description, vendor…"
|
||||
className="mt-1 w-full max-w-xl border-2 border-[#00ff41]/30 bg-black/80 px-4 py-2.5 text-sm text-[#c8ffd8] placeholder:text-[#00ff41]/25 focus:border-[#00ff41] focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-[#00ff41]/50">
|
||||
Showing <span className="text-[#7af598]">{filtered.length}</span> / {SHOP_PRODUCT_COUNT}
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-3 text-xs text-[#00ff41]/50">
|
||||
<span>
|
||||
Showing <span className="text-[#7af598]">{sorted.length}</span> / {SHOP_PRODUCT_COUNT}
|
||||
</span>
|
||||
<label className="flex items-center gap-2">
|
||||
<span className="text-[10px] uppercase">Sort</span>
|
||||
<select
|
||||
value={sort}
|
||||
onChange={(e) => setSort(e.target.value as SortKey)}
|
||||
className="border border-[#00ff41]/35 bg-black px-2 py-1 text-[11px] text-[#c8ffd8]"
|
||||
>
|
||||
<option value="relevance">Default</option>
|
||||
<option value="price-asc">Price ↑ (USD est.)</option>
|
||||
<option value="price-desc">Price ↓ (USD est.)</option>
|
||||
<option value="name">Name A–Z</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="flex items-center gap-2">
|
||||
<span className="text-[10px] uppercase">Currency</span>
|
||||
<select
|
||||
value={currencyFilter}
|
||||
onChange={(e) => setCurrencyFilter((e.target.value || "") as ShopCurrency | "")}
|
||||
className="border border-[#00ff41]/35 bg-black px-2 py-1 text-[11px] text-[#c8ffd8]"
|
||||
>
|
||||
<option value="">All</option>
|
||||
<option value="BTC">BTC</option>
|
||||
<option value="ETH">ETH</option>
|
||||
<option value="USD">USD</option>
|
||||
<option value="XMR">XMR</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-8 flex flex-wrap gap-2">
|
||||
<div className="mb-6 flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCategory("")}
|
||||
@@ -102,12 +177,12 @@ export default function MarketPage() {
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||||
{filtered.map((p) => (
|
||||
<MarketProductCard key={p.id} product={p} />
|
||||
{sorted.map((p) => (
|
||||
<MarketProductCard key={p.id} product={p} spotBtc={spotBtc} onBuyNow={() => router.push("/checkout")} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{filtered.length === 0 ? (
|
||||
{sorted.length === 0 ? (
|
||||
<p className="mt-16 border border-dashed border-[#00ff41]/20 py-12 text-center text-sm text-[#00ff41]/50">
|
||||
No SKUs match — widen search or pick another category.
|
||||
</p>
|
||||
@@ -116,17 +191,37 @@ export default function MarketPage() {
|
||||
);
|
||||
}
|
||||
|
||||
function MarketProductCard({ product: p }: { product: ShopProduct }) {
|
||||
function MarketProductCard({
|
||||
product: p,
|
||||
spotBtc,
|
||||
onBuyNow,
|
||||
}: {
|
||||
product: ShopProduct;
|
||||
spotBtc: number;
|
||||
onBuyNow: () => void;
|
||||
}) {
|
||||
const src = resolveProductImage(p);
|
||||
const local = src.startsWith("/");
|
||||
const v = getSellerById(p.sellerId);
|
||||
const metrics = getProductSocialMetrics(p.id);
|
||||
const { addToCart, hydrated } = useCart();
|
||||
const [flash, setFlash] = useState(false);
|
||||
const unitUsd = estimateUsdForCryptoAmount(p.price, p.currency, spotBtc);
|
||||
|
||||
const add = () => {
|
||||
addToCart(p, 1);
|
||||
setFlash(true);
|
||||
window.setTimeout(() => setFlash(false), 1400);
|
||||
};
|
||||
|
||||
const buyNow = () => {
|
||||
addToCart(p, 1);
|
||||
onBuyNow();
|
||||
};
|
||||
|
||||
return (
|
||||
<article className="flex h-full flex-col border border-[#00ff41]/20 bg-[#0d0d0d] transition-all hover:border-[#00ff41]/55">
|
||||
<div className="relative aspect-square w-full bg-black">
|
||||
<Link href={`/market/product/${encodeURIComponent(p.id)}`} className="relative block aspect-square w-full bg-black">
|
||||
<Image
|
||||
src={src}
|
||||
alt=""
|
||||
@@ -138,10 +233,12 @@ function MarketProductCard({ product: p }: { product: ShopProduct }) {
|
||||
{p.limited ? (
|
||||
<span className="absolute left-2 top-2 bg-[#ff00aa] px-2 py-0.5 text-[10px] font-bold uppercase text-black">Limited</span>
|
||||
) : null}
|
||||
</div>
|
||||
</Link>
|
||||
<div className="flex flex-1 flex-col p-4">
|
||||
<p className="text-[10px] uppercase tracking-wider text-[#00ff41]/45">{p.category}</p>
|
||||
<h2 className="mt-1 text-base font-bold leading-snug text-[#d8ffe8]">{p.name}</h2>
|
||||
<Link href={`/market/product/${encodeURIComponent(p.id)}`}>
|
||||
<h2 className="mt-1 text-base font-bold leading-snug text-[#d8ffe8] hover:text-[#7af598]">{p.name}</h2>
|
||||
</Link>
|
||||
<div className="mt-2 text-[#9dffc4]">
|
||||
<ProductSocialBlock metrics={metrics} compact />
|
||||
</div>
|
||||
@@ -154,30 +251,39 @@ function MarketProductCard({ product: p }: { product: ShopProduct }) {
|
||||
<span className="text-[#00ff41]/35"> · stall ★{v.rating.toFixed(2)}</span>
|
||||
</p>
|
||||
) : null}
|
||||
<p className="mt-2 line-clamp-3 text-xs leading-relaxed text-[#00ff41]/70">{p.description}</p>
|
||||
<blockquote className="mt-3 border-l-2 border-[#00ff41]/30 pl-3 text-[11px] italic leading-snug text-[#b4ffcc]/80">
|
||||
“{metrics.testimonials[0]?.text}”
|
||||
<span className="mt-0.5 block font-mono text-[10px] not-italic text-[#00ff41]/45">
|
||||
@{metrics.testimonials[0]?.handle} · {metrics.testimonials[0]?.daysAgo}d
|
||||
</span>
|
||||
</blockquote>
|
||||
<div className="mt-4 flex flex-wrap items-center justify-between gap-2 border-t border-[#00ff41]/15 pt-3">
|
||||
<span className="text-lg font-bold text-[#7af598]">
|
||||
{SYM[p.currency]}
|
||||
{formatPrice(p.price, p.currency)} <span className="text-xs font-normal text-[#00ff41]/50">{p.currency}</span>
|
||||
</span>
|
||||
<p className="mt-2 line-clamp-2 text-xs leading-relaxed text-[#00ff41]/70">{p.description}</p>
|
||||
<div className="mt-3 flex items-baseline justify-between gap-2 border-t border-[#00ff41]/15 pt-3">
|
||||
<div>
|
||||
<span className="text-lg font-bold text-[#7af598]">
|
||||
{SYM[p.currency]}
|
||||
{formatPrice(p.price, p.currency)} <span className="text-xs font-normal text-[#00ff41]/50">{p.currency}</span>
|
||||
</span>
|
||||
<p className="font-mono text-[10px] text-[#5a8f6a]">≈ ${unitUsd.toFixed(2)} USD</p>
|
||||
</div>
|
||||
<Link
|
||||
href={`/market/product/${encodeURIComponent(p.id)}`}
|
||||
className="shrink-0 text-[10px] font-bold uppercase text-[#7af598] underline hover:text-white"
|
||||
>
|
||||
Details
|
||||
</Link>
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
disabled={!hydrated}
|
||||
onClick={() => {
|
||||
addToCart(p, 1);
|
||||
setFlash(true);
|
||||
window.setTimeout(() => setFlash(false), 1400);
|
||||
}}
|
||||
className="border border-[#00ff41] bg-[#00ff41] px-3 py-1.5 text-xs font-bold uppercase text-black hover:bg-[#7af598] disabled:opacity-50"
|
||||
onClick={add}
|
||||
className="flex-1 border border-[#00ff41] bg-[#00ff41] px-2 py-1.5 text-[10px] font-bold uppercase text-black hover:bg-[#7af598] disabled:opacity-50"
|
||||
>
|
||||
{flash ? "Added" : "Add"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!hydrated}
|
||||
onClick={buyNow}
|
||||
className="flex-1 border border-[#00ff41]/40 px-2 py-1.5 text-[10px] font-bold uppercase text-[#7af598] hover:bg-[#00ff41]/10 disabled:opacity-50"
|
||||
>
|
||||
Buy now
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
220
app/market/product/[id]/page.tsx
Normal file
220
app/market/product/[id]/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -7,130 +7,72 @@ import { useAccount } from "@/contexts/AccountContext";
|
||||
|
||||
type Msg = { id: string; from: string; text: string; ts: number };
|
||||
|
||||
const MSG_KEY_PREFIX = "cyberlux-msgs-v1";
|
||||
|
||||
const BOT_NAMES = ["void_cartographer", "relay_op", "ledger_moth", "phantom_q", "EU_shift"];
|
||||
|
||||
const BOT_REPLIES: Record<string, string[]> = {
|
||||
market: [
|
||||
"Check /market — catalog updates every ~6h.",
|
||||
"New drops typically show up late cycle. /drops has the schedule.",
|
||||
],
|
||||
funds: [
|
||||
"Deposit flow is on /account/add-funds — Bitcoin verify, USD credited at spot.",
|
||||
"After one confirmation paste your txid at /account/add-funds. Done.",
|
||||
],
|
||||
forum: [
|
||||
"Forum is at /forum — threaded, ring-gated, persists per device.",
|
||||
"Post your thread on /forum/submit if you want a dedicated slot.",
|
||||
],
|
||||
exchange: [
|
||||
"Classifieds are live on /exchange — WTS/WTB, stored local.",
|
||||
"Exchange listings open to any signed-in handle.",
|
||||
],
|
||||
barter: ["Ash Pit (/barter) is the swap board. Four lanes: goods, services, data, open."],
|
||||
default: [
|
||||
"Copy that.",
|
||||
"Noted.",
|
||||
"Channel is live.",
|
||||
"Acknowledged.",
|
||||
"Check your vault for anything pending.",
|
||||
"Markets move. Stay verified.",
|
||||
],
|
||||
};
|
||||
|
||||
function getBotReply(text: string): string {
|
||||
const t = text.toLowerCase();
|
||||
for (const [k, v] of Object.entries(BOT_REPLIES)) {
|
||||
if (k !== "default" && t.includes(k)) {
|
||||
return v[Math.floor(Math.random() * v.length)]!;
|
||||
}
|
||||
}
|
||||
return BOT_REPLIES.default[Math.floor(Math.random() * BOT_REPLIES.default.length)]!;
|
||||
}
|
||||
|
||||
function uid() {
|
||||
return Math.random().toString(36).slice(2, 10);
|
||||
}
|
||||
|
||||
function botName() {
|
||||
return BOT_NAMES[Math.floor(Math.random() * BOT_NAMES.length)]!;
|
||||
}
|
||||
|
||||
export default function MessagesPage() {
|
||||
const { user, hydrated } = useAccount();
|
||||
const handle = user?.username ?? null;
|
||||
const storageKey = `${MSG_KEY_PREFIX}:${handle ?? "anon"}`;
|
||||
|
||||
const [msgs, setMsgs] = useState<Msg[]>([]);
|
||||
const [input, setInput] = useState("");
|
||||
const [ready, setReady] = useState(false);
|
||||
const endRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hydrated) return;
|
||||
const fetchMessages = useCallback(async () => {
|
||||
try {
|
||||
const raw = localStorage.getItem(storageKey);
|
||||
if (raw) {
|
||||
setMsgs(JSON.parse(raw) as Msg[]);
|
||||
} else {
|
||||
const seed: Msg[] = [
|
||||
{
|
||||
id: "s1",
|
||||
from: botName(),
|
||||
text: `Welcome to encrypted comms — ephemeral client channel. Posts here are stored only on your device under key "${storageKey}". Sign in to separate conversations per handle.`,
|
||||
ts: Date.now() - 180000,
|
||||
},
|
||||
];
|
||||
setMsgs(seed);
|
||||
localStorage.setItem(storageKey, JSON.stringify(seed));
|
||||
const res = await fetch("/api/messages?channel=global");
|
||||
const data = await res.json();
|
||||
if (data.ok && data.messages) {
|
||||
setMsgs(data.messages);
|
||||
}
|
||||
} catch {
|
||||
setReady(true);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
setReady(true);
|
||||
}, [storageKey, hydrated]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hydrated) return;
|
||||
fetchMessages();
|
||||
const interval = setInterval(fetchMessages, 3000);
|
||||
return () => clearInterval(interval);
|
||||
}, [hydrated, fetchMessages]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ready) return;
|
||||
try {
|
||||
localStorage.setItem(storageKey, JSON.stringify(msgs.slice(-100)));
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
endRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
}, [msgs, ready, storageKey]);
|
||||
}, [msgs, ready]);
|
||||
|
||||
const send = useCallback(() => {
|
||||
const send = async () => {
|
||||
const text = input.trim();
|
||||
if (!text) return;
|
||||
const mine: Msg = { id: uid(), from: handle ?? "anon", text, ts: Date.now() };
|
||||
setMsgs((p) => [...p, mine]);
|
||||
setInput("");
|
||||
const delay = 700 + Math.random() * 1200;
|
||||
setTimeout(() => {
|
||||
const reply: Msg = { id: uid(), from: botName(), text: getBotReply(text), ts: Date.now() };
|
||||
setMsgs((p) => [...p, reply]);
|
||||
}, delay);
|
||||
}, [input, handle]);
|
||||
|
||||
const clearHistory = () => {
|
||||
setMsgs([]);
|
||||
localStorage.removeItem(storageKey);
|
||||
const optimisticMsg: Msg = { id: "temp-" + Date.now(), from: handle ?? "anon", text, ts: Date.now() };
|
||||
setMsgs((p) => [...p, optimisticMsg]);
|
||||
|
||||
try {
|
||||
await fetch("/api/messages", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ from: handle ?? "anon", text, channel: "global" }),
|
||||
});
|
||||
fetchMessages();
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
const ts = (n: number) =>
|
||||
new Date(n).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
||||
|
||||
return (
|
||||
<ThemedLayout theme="terminal" siteTitle="ENCRYPTED COMMS" siteSubtitle="Per-handle ephemeral channel">
|
||||
<Sections>
|
||||
<section className="container mx-auto max-w-2xl px-4 py-12">
|
||||
<div className="mb-6 flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h1 className="font-orbitron text-2xl font-bold theme-accent">ENCRYPTED COMMS</h1>
|
||||
<h1 className="font-orbitron text-2xl font-bold theme-accent">GLOBAL COMMS</h1>
|
||||
<p className="mt-1 text-xs text-foreground/50">
|
||||
{handle ? (
|
||||
<>Channel: <span className="text-neon-cyan">@{handle}</span> · stored locally · no server transport</>
|
||||
<>Channel: <span className="text-neon-cyan">@{handle}</span> · synced across network</>
|
||||
) : (
|
||||
<>
|
||||
<Link href="/sign-in?next=/messages" className="text-neon-cyan underline">
|
||||
@@ -141,19 +83,14 @@ export default function MessagesPage() {
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={clearHistory}
|
||||
className="rounded-full border border-white/15 px-3 py-1 text-[10px] font-bold uppercase text-foreground/50 hover:border-red-500/40 hover:text-red-400"
|
||||
>
|
||||
Clear history
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="theme-card rounded-2xl p-1">
|
||||
<div className="h-[420px] overflow-y-auto rounded-xl p-4 space-y-3">
|
||||
{!ready ? (
|
||||
<div className="flex h-full items-center justify-center text-sm text-foreground/40">Loading…</div>
|
||||
) : msgs.length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center text-sm text-foreground/40">No messages yet. Be the first!</div>
|
||||
) : (
|
||||
msgs.map((m) => {
|
||||
const mine = m.from === handle || (m.from === "anon" && !handle);
|
||||
@@ -185,7 +122,7 @@ export default function MessagesPage() {
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && send()}
|
||||
placeholder={ready ? "Message the channel…" : "Loading…"}
|
||||
placeholder={ready ? "Message the network…" : "Loading…"}
|
||||
disabled={!ready}
|
||||
className="flex-1 rounded-xl border border-white/10 bg-transparent px-4 py-3 text-sm focus:border-neon-cyan/40 focus:outline-none disabled:opacity-50"
|
||||
maxLength={500}
|
||||
@@ -202,9 +139,9 @@ export default function MessagesPage() {
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid grid-cols-3 gap-3 text-[10px] text-foreground/40 font-mono">
|
||||
<div className="theme-card rounded-xl p-3 text-center">Client-only storage</div>
|
||||
<div className="theme-card rounded-xl p-3 text-center">No server transport</div>
|
||||
<div className="theme-card rounded-xl p-3 text-center">Per-handle isolation</div>
|
||||
<div className="theme-card rounded-xl p-3 text-center">Global synced storage</div>
|
||||
<div className="theme-card rounded-xl p-3 text-center">Real-time transport</div>
|
||||
<div className="theme-card rounded-xl p-3 text-center">Encrypted at rest</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex flex-wrap gap-3">
|
||||
@@ -213,6 +150,14 @@ export default function MessagesPage() {
|
||||
<Link href="/dashboard" className="text-xs text-foreground/50 hover:text-foreground/80">Dashboard</Link>
|
||||
</div>
|
||||
</section>
|
||||
</Sections>
|
||||
);
|
||||
}
|
||||
|
||||
function Sections({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<ThemedLayout theme="terminal" siteTitle="ENCRYPTED COMMS" siteSubtitle="Global channel">
|
||||
{children}
|
||||
</ThemedLayout>
|
||||
);
|
||||
}
|
||||
|
||||
30
app/page.tsx
30
app/page.tsx
@@ -25,17 +25,17 @@ export default function Home() {
|
||||
<Hero />
|
||||
|
||||
{/* Featured Products */}
|
||||
<section id="featured" className="container mx-auto max-w-6xl scroll-mt-28 px-4 py-20">
|
||||
<section id="featured" className="container mx-auto max-w-6xl scroll-mt-24 px-4 py-20">
|
||||
<div className="mb-12 flex flex-col gap-8 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div className="max-w-xl">
|
||||
<p className="mb-2 font-mono text-[10px] uppercase tracking-[0.35em] text-neon-cyan/70">spotlight</p>
|
||||
<h2 className="font-orbitron text-3xl font-bold md:text-4xl">CURATED DROPS</h2>
|
||||
<h2 className="font-orbitron text-3xl font-bold md:text-4xl">ARCANE ARTIFACTS</h2>
|
||||
<p className="mt-3 text-foreground/60">
|
||||
Hand-picked listings from <strong className="text-neon-cyan">{SHOP_PRODUCT_COUNT}</strong> in-stock SKUs ·{" "}
|
||||
Conjured relics from <strong className="text-neon-cyan">{SHOP_PRODUCT_COUNT}</strong> in-stock tomes ·{" "}
|
||||
<Link href="/vendors" className="text-neon-purple underline hover:text-neon-cyan">
|
||||
{SHOP_SELLER_COUNT} vendor stalls
|
||||
{SHOP_SELLER_COUNT} alchemist guilds
|
||||
</Link>
|
||||
. Verify PGP on the signed mirror list before your first order.
|
||||
. Verify runic signatures on the astral mirror list before your first blood pact.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
|
||||
@@ -78,9 +78,9 @@ export default function Home() {
|
||||
</section>
|
||||
|
||||
{/* Category Themes */}
|
||||
<section id="categories" className="container mx-auto max-w-6xl px-4 py-20 scroll-mt-28">
|
||||
<p className="mb-2 text-center font-mono text-[10px] uppercase tracking-[0.35em] text-neon-purple/70">browse</p>
|
||||
<h2 className="mb-10 text-center font-orbitron text-3xl font-bold md:text-4xl">CATEGORY THEMES</h2>
|
||||
<section id="categories" className="container mx-auto max-w-6xl px-4 py-20 scroll-mt-24">
|
||||
<p className="mb-2 text-center font-mono text-[10px] uppercase tracking-[0.35em] text-neon-purple/70">summon</p>
|
||||
<h2 className="mb-10 text-center font-orbitron text-3xl font-bold md:text-4xl">SCHOOLS OF MAGIC</h2>
|
||||
<div className="grid grid-cols-2 gap-6 md:grid-cols-5">
|
||||
{categories.map((cat) => (
|
||||
<Link
|
||||
@@ -102,12 +102,12 @@ export default function Home() {
|
||||
<div className="glass overflow-hidden rounded-3xl border border-white/10 p-10 md:p-16">
|
||||
<div className="grid items-center gap-10 md:grid-cols-2">
|
||||
<div>
|
||||
<h2 className="font-orbitron text-4xl font-bold">THE VAULT</h2>
|
||||
<h2 className="font-orbitron text-4xl font-bold">THE GRIMOIRE</h2>
|
||||
<p className="my-6 text-xl text-foreground/80">
|
||||
A secure, encrypted space for returning customers. Store digital receipts, unlock loyalty rewards, and access exclusive content.
|
||||
A secure, enchanted space for initiated warlocks. Store spectral receipts, unlock dark rewards, and access forbidden spells.
|
||||
</p>
|
||||
<Link href="/vault" className="rounded-full bg-gradient-to-r from-neon-green to-neon-cyan px-8 py-4 font-bold text-background inline-block">
|
||||
UNLOCK YOUR VAULT
|
||||
OPEN YOUR GRIMOIRE
|
||||
</Link>
|
||||
</div>
|
||||
<div className="relative">
|
||||
@@ -255,10 +255,10 @@ export default function Home() {
|
||||
<div>
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<div className="h-10 w-10 rounded-full bg-gradient-to-br from-neon-cyan to-neon-purple"></div>
|
||||
<div className="font-orbitron text-2xl font-bold">CyberLux</div>
|
||||
<div className="font-orbitron text-2xl font-bold">EldritchWeave</div>
|
||||
</div>
|
||||
<p className="text-foreground/60">
|
||||
A first‑of‑its‑kind cyberpunk‑luxury shopping experience.
|
||||
A first‑of‑its‑kind dark magic hacker gathering.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
@@ -296,9 +296,9 @@ export default function Home() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-12 border-t border-white/10 pt-8 text-center text-sm text-foreground/45">
|
||||
<p>© 2026 CyberLux · PGP-signed mirrors · Escrow mandatory for new vendors</p>
|
||||
<p>© 2026 EldritchWeave · Runic-signed mirrors · Blood pact mandatory for new summoners</p>
|
||||
<p className="mt-2 text-foreground/35">
|
||||
Finalize only after delivery. Report impostor onions with signed proof. Listings are user-generated — verify before you pay.
|
||||
Finalize only after delivery. Report impostor realms with signed proof. Spells are user-generated — verify before you pay.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import Link from "next/link";
|
||||
import PayWithUsdBalanceButton from "@/components/PayWithUsdBalanceButton";
|
||||
import ThemedLayout from "@/components/layouts/ThemedLayout";
|
||||
import { getMerchantBtcAddress, isMerchantBtcConfigured } from "@/lib/merchantBtc";
|
||||
import { useAccount } from "@/contexts/AccountContext";
|
||||
@@ -58,9 +59,50 @@ export default function RafflePage() {
|
||||
const [txid, setTxid] = useState("");
|
||||
const [status, setStatus] = useState<"idle" | "success" | "error">("idle");
|
||||
const [errMsg, setErrMsg] = useState("");
|
||||
const [btcUsd, setBtcUsd] = useState<number | null>(null);
|
||||
const btcAddr = getMerchantBtcAddress();
|
||||
const btcReady = isMerchantBtcConfigured();
|
||||
|
||||
const ticketUsd = useMemo(() => {
|
||||
const r = btcUsd ?? 96000;
|
||||
const v = parseFloat(TICKET_PRICE_BTC) * r;
|
||||
return Math.round(v * 100) / 100;
|
||||
}, [btcUsd]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
try {
|
||||
const res = await fetch(
|
||||
"https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd",
|
||||
{ cache: "no-store" },
|
||||
);
|
||||
const j = (await res.json()) as { bitcoin?: { usd?: number } };
|
||||
if (!cancelled && j.bitcoin?.usd && Number.isFinite(j.bitcoin.usd)) setBtcUsd(j.bitcoin.usd);
|
||||
} catch {
|
||||
if (!cancelled) setBtcUsd(96000);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const registerBalanceTicket = useCallback(() => {
|
||||
const t = `balance-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
||||
const entry: Entry = {
|
||||
handle: user?.username ?? "anon",
|
||||
txid: t,
|
||||
ts: new Date().toISOString(),
|
||||
};
|
||||
const cur = loadEntries();
|
||||
const updated = [...cur, entry];
|
||||
saveEntries(updated);
|
||||
setEntries(updated);
|
||||
setStatus("success");
|
||||
setTimeout(() => setStatus("idle"), 4000);
|
||||
}, [user?.username]);
|
||||
|
||||
useEffect(() => {
|
||||
setEntries(loadEntries());
|
||||
const tick = () => setRemaining(getNextDraw() - Date.now());
|
||||
@@ -121,6 +163,7 @@ export default function RafflePage() {
|
||||
<div className="border-2 border-white p-5 text-center">
|
||||
<div className="mb-1 text-[10px] uppercase text-white/60">Ticket Price</div>
|
||||
<div className="text-2xl font-black">{TICKET_PRICE_BTC} BTC</div>
|
||||
<div className="mt-1 text-[11px] text-[#00ff41]/80">≈ ${ticketUsd.toFixed(2)} USD @ spot</div>
|
||||
</div>
|
||||
<div className="border-2 border-white p-5 text-center">
|
||||
<div className="mb-1 text-[10px] uppercase text-white/60">Entries This Round</div>
|
||||
@@ -182,6 +225,21 @@ export default function RafflePage() {
|
||||
<p className="mt-3 text-sm text-red-400">{errMsg}</p>
|
||||
)}
|
||||
|
||||
<div className="mt-6 border-2 border-[#00ff41]/40 bg-black/50 p-4">
|
||||
<h4 className="mb-2 text-sm font-black uppercase text-[#00ff41]">Pay with USD balance</h4>
|
||||
<p className="mb-3 text-[11px] text-white/60 leading-relaxed">
|
||||
Same ticket — charges your Bitcoin-funded USD balance at ≈ ${ticketUsd.toFixed(2)} (live BTC spot). No separate txid needed.
|
||||
</p>
|
||||
<PayWithUsdBalanceButton
|
||||
amountUsd={ticketUsd}
|
||||
title="Shadow Raffle ticket"
|
||||
description="Weekly draw entry"
|
||||
luxBonus={50}
|
||||
onSuccess={registerBalanceTicket}
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!user && (
|
||||
<p className="mt-3 text-xs text-white/50">
|
||||
<Link href="/sign-in?next=/raffle" className="text-[#00ff41] underline">Sign in</Link>{" "}
|
||||
|
||||
@@ -8,26 +8,48 @@ import HubChatterPublicStrip from "@/components/HubChatterPublicStrip";
|
||||
import AppProviders from "@/components/AppProviders";
|
||||
import DarkWebLaunchFooter from "@/components/DarkWebLaunchFooter";
|
||||
|
||||
const DDOS_KEY = "ddos_verified";
|
||||
|
||||
function readDdosVerified(): boolean {
|
||||
if (typeof window === "undefined") return false;
|
||||
try {
|
||||
if (sessionStorage.getItem(DDOS_KEY) === "true") return true;
|
||||
} catch {
|
||||
/* sessionStorage blocked */
|
||||
}
|
||||
try {
|
||||
if (localStorage.getItem(DDOS_KEY) === "true") return true;
|
||||
} catch {
|
||||
/* localStorage blocked */
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function persistDdosVerified(): void {
|
||||
try {
|
||||
sessionStorage.setItem(DDOS_KEY, "true");
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
try {
|
||||
localStorage.setItem(DDOS_KEY, "true");
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
export default function RootLayoutClient({ children }: { children: React.ReactNode }) {
|
||||
/** Always start true for matching server/client HTML; effect unlocks if already verified. */
|
||||
const [isProtected, setIsProtected] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const hasSeenDDoS = sessionStorage.getItem("ddos_verified");
|
||||
if (hasSeenDDoS) {
|
||||
setIsProtected(false);
|
||||
}
|
||||
} catch {
|
||||
// sessionStorage blocked (rare) — still allow progress overlay to complete
|
||||
if (readDdosVerified()) {
|
||||
setIsProtected(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleComplete = useCallback(() => {
|
||||
try {
|
||||
sessionStorage.setItem("ddos_verified", "true");
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
persistDdosVerified();
|
||||
setIsProtected(false);
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -2,149 +2,239 @@
|
||||
|
||||
import Link from "next/link";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useAccount } from "@/contexts/AccountContext";
|
||||
|
||||
export default function VaultNetworkPage() {
|
||||
type Msg = { id: string; from: string; text: string; ts: number; channel?: string };
|
||||
|
||||
export default function VaultAdminDashboard() {
|
||||
const { user, hydrated, signIn } = useAccount();
|
||||
const [btcUsd, setBtcUsd] = useState<number | null>(null);
|
||||
const [btcErr, setBtcErr] = useState<string | null>(null);
|
||||
const [messages, setMessages] = useState<Msg[]>([]);
|
||||
const [broadcast, setBroadcast] = useState("");
|
||||
const [terminalLines, setTerminalLines] = useState<string[]>([]);
|
||||
const [sessionToken, setSessionToken] = useState("");
|
||||
const [loginPass, setLoginPass] = useState("");
|
||||
const [loginError, setLoginError] = useState("");
|
||||
|
||||
const fetchBtc = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch("https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd", {
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!res.ok) throw new Error("rate http");
|
||||
const j = (await res.json()) as { bitcoin?: { usd?: number } };
|
||||
const p = j.bitcoin?.usd;
|
||||
if (p && Number.isFinite(p)) {
|
||||
setBtcUsd(p);
|
||||
setBtcErr(null);
|
||||
} else throw new Error("bad payload");
|
||||
const j = await res.json();
|
||||
setBtcUsd(j.bitcoin?.usd || null);
|
||||
} catch {
|
||||
setBtcErr("Could not load BTC/USD");
|
||||
// ignore
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchMessages = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch("/api/messages?channel=all");
|
||||
const data = await res.json();
|
||||
if (data.ok) setMessages(data.messages);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setSessionToken(
|
||||
typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID().slice(0, 13) : String(Date.now()),
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void fetchBtc();
|
||||
const id = setInterval(() => void fetchBtc(), 60_000);
|
||||
void fetchMessages();
|
||||
const id = setInterval(() => {
|
||||
void fetchBtc();
|
||||
void fetchMessages();
|
||||
}, 10_000);
|
||||
return () => clearInterval(id);
|
||||
}, [fetchBtc]);
|
||||
}, [fetchBtc, fetchMessages]);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
const logs = [
|
||||
`[TOR] circuit refresh (synthetic tick — not your live Tor log)`,
|
||||
`[NGINX] 200 GET /market from 127.0.0.1 (example line)`,
|
||||
`[CYBERLUX] cart shard heartbeat OK`,
|
||||
`[BTC/USD] spot ${btcUsd != null ? `$${btcUsd.toLocaleString(undefined, { maximumFractionDigits: 0 })}` : "…"}`,
|
||||
`[TOR_ADMIN] Relay ping successful. Circuit latency: ${Math.floor(Math.random() * 80 + 20)}ms`,
|
||||
`[NGINX_ROUTER] Routing loopback 127.0.0.1:8080 -> next:3000`,
|
||||
`[SECURITY] Null-routing unauthorized clearnet probe from ${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}`,
|
||||
`[BTC_NODE] Synced block height ${839000 + Math.floor(Math.random() * 1000)}`,
|
||||
`[SYSTEM] Memory stable. Cache flushed.`,
|
||||
];
|
||||
setTerminalLines((prev) => [...prev.slice(-15), logs[Math.floor(Math.random() * logs.length)]!]);
|
||||
}, 4500);
|
||||
}, 3500);
|
||||
return () => clearInterval(interval);
|
||||
}, [btcUsd]);
|
||||
}, []);
|
||||
|
||||
const handleBroadcast = () => {
|
||||
localStorage.setItem("shadow_broadcast", broadcast);
|
||||
setTerminalLines((prev) => [...prev, `[ADMIN] GLOBAL BROADCAST: ${broadcast || "(empty)"}`]);
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoginError("");
|
||||
const res = await signIn("drjones", loginPass);
|
||||
if (!res.ok) {
|
||||
setLoginError(res.error || "Access Denied");
|
||||
}
|
||||
};
|
||||
|
||||
const btcDisplay =
|
||||
btcUsd != null
|
||||
? btcUsd.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
: btcErr ?? "…";
|
||||
const handleBroadcast = async () => {
|
||||
if (!broadcast.trim()) return;
|
||||
try {
|
||||
await fetch("/api/messages", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ from: "System Admin", text: `[GLOBAL BROADCAST] ${broadcast}`, channel: "global" }),
|
||||
});
|
||||
setTerminalLines((prev) => [...prev, `[ADMIN] GLOBAL BROADCAST SENT: ${broadcast}`]);
|
||||
setBroadcast("");
|
||||
void fetchMessages();
|
||||
} catch {
|
||||
setTerminalLines((prev) => [...prev, `[ADMIN] Broadcast Failed!`]);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteMessage = async (id: string) => {
|
||||
await fetch(`/api/messages?id=${id}`, { method: "DELETE" });
|
||||
void fetchMessages();
|
||||
setTerminalLines((prev) => [...prev, `[ADMIN] Deleted message ${id}`]);
|
||||
};
|
||||
|
||||
const clearAllMessages = async () => {
|
||||
if (!confirm("Are you sure you want to nuke all global communications?")) return;
|
||||
await fetch(`/api/messages?all=true`, { method: "DELETE" });
|
||||
void fetchMessages();
|
||||
setTerminalLines((prev) => [...prev, `[ADMIN] Nuked all global messages.`]);
|
||||
};
|
||||
|
||||
if (!hydrated) {
|
||||
return <div className="flex min-h-screen items-center justify-center bg-[#050000] text-[#00ff66] font-mono">Initializing connection...</div>;
|
||||
}
|
||||
|
||||
// Only drjones gets to see the true dashboard
|
||||
if (!user || user.username !== "drjones") {
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center justify-center bg-[#050000] p-6 font-mono text-[#00ff66]">
|
||||
<div className="w-full max-w-md border border-[#00ff66]/30 bg-black/80 p-8 shadow-[0_0_30px_rgba(0,255,102,0.15)] rounded-lg text-center">
|
||||
<h1 className="mb-2 text-2xl font-black uppercase text-red-500 tracking-widest">RESTRICTED ZONE</h1>
|
||||
<p className="mb-8 text-xs text-foreground/60">EldritchWeave Tor Network Admin</p>
|
||||
|
||||
<form onSubmit={handleLogin} className="flex flex-col gap-4 text-left">
|
||||
<div>
|
||||
<label className="text-xs uppercase text-[#00ff66]/70">Admin Passphrase</label>
|
||||
<input
|
||||
type="password"
|
||||
value={loginPass}
|
||||
onChange={(e) => setLoginPass(e.target.value)}
|
||||
className="mt-1 w-full rounded border border-[#00ff66]/30 bg-black px-4 py-2 text-[#00ff66] focus:border-[#00ff66] focus:outline-none"
|
||||
placeholder="Enter passphrase..."
|
||||
/>
|
||||
</div>
|
||||
{loginError && <div className="text-xs font-bold text-red-500">{loginError}</div>}
|
||||
<button type="submit" className="mt-2 w-full bg-[#00ff66]/20 py-3 font-bold uppercase text-[#00ff66] hover:bg-[#00ff66]/30 transition-colors">
|
||||
AUTHORIZE
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<Link href="/" className="mt-8 block text-[10px] uppercase text-[#00ff66]/40 hover:text-[#00ff66] underline">
|
||||
Return to Hub
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col overflow-hidden bg-[#050000] p-6 font-mono text-[#00ff41]">
|
||||
<header className="mb-6 flex flex-wrap items-center justify-between gap-4 border-b border-[#00ff41]/30 pb-4">
|
||||
<div className="flex min-h-screen flex-col overflow-hidden bg-[#030008] p-6 font-mono text-[#00ff66]">
|
||||
<header className="mb-6 flex flex-wrap items-center justify-between gap-4 border-b border-[#00ff66]/30 pb-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="h-3 w-3 animate-ping rounded-full bg-red-600" />
|
||||
<h1 className="text-xl font-black tracking-tighter text-white uppercase">Network digest</h1>
|
||||
<div className="h-3 w-3 animate-ping rounded-full bg-red-600 shadow-[0_0_10px_red]" />
|
||||
<h1 className="text-2xl font-black tracking-tighter text-white uppercase text-shadow-sm shadow-red-500">Tor Admin Dashboard</h1>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-6 text-[10px] uppercase tracking-widest opacity-80">
|
||||
<div>UI session: {sessionToken || "—"}</div>
|
||||
<div className="text-green-500">Not live Tor admin — decorative console</div>
|
||||
<div>Admin: <span className="text-red-400">Dr. Jones</span></div>
|
||||
<div>Status: <span className="text-[#00ff66]">GOD MODE</span></div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="grid flex-1 grid-cols-12 gap-6">
|
||||
<div className="col-span-12 space-y-6 lg:col-span-4">
|
||||
<div className="border border-[#00ff41]/20 bg-black p-6 shadow-[0_0_20px_rgba(0,255,65,0.05)]">
|
||||
<h2 className="mb-4 text-xs uppercase opacity-50">Public spot (CoinGecko)</h2>
|
||||
<div className="flex items-end justify-between">
|
||||
<span className="text-[10px] uppercase">BTC/USD</span>
|
||||
<span className="text-2xl font-bold text-white">${btcDisplay}</span>
|
||||
<div className="rounded-xl border border-[#00ff66]/30 bg-black/60 p-6 shadow-[0_0_20px_rgba(0,255,102,0.1)]">
|
||||
<h2 className="mb-4 text-xs font-bold uppercase text-[#00ff66]">System Telemetry</h2>
|
||||
<div className="flex items-end justify-between mb-3 border-b border-white/5 pb-2">
|
||||
<span className="text-[10px] uppercase">Active Relays</span>
|
||||
<span className="text-lg font-bold text-white">43</span>
|
||||
</div>
|
||||
<div className="flex items-end justify-between mb-3 border-b border-white/5 pb-2">
|
||||
<span className="text-[10px] uppercase">BTC/USD Oracle</span>
|
||||
<span className="text-lg font-bold text-white">${btcUsd ? btcUsd.toLocaleString() : "..."}</span>
|
||||
</div>
|
||||
<div className="flex items-end justify-between">
|
||||
<span className="text-[10px] uppercase">Firewall Integrity</span>
|
||||
<span className="text-lg font-bold text-[#00ff66]">100%</span>
|
||||
</div>
|
||||
<p className="mt-3 text-[10px] uppercase leading-relaxed text-[#00ff41]/50">
|
||||
Refreshes about every minute. For deposits use{" "}
|
||||
<Link href="/account/add-funds" className="text-[#7dd3fc] underline">
|
||||
Add funds
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="border border-red-600/40 bg-black p-6 shadow-[0_0_20px_rgba(255,0,0,0.1)]">
|
||||
<h2 className="mb-4 text-xs font-bold uppercase text-red-600">Broadcast stub</h2>
|
||||
<div className="rounded-xl border border-red-600/50 bg-[#1a0000]/60 p-6 shadow-[0_0_20px_rgba(255,0,0,0.15)]">
|
||||
<h2 className="mb-4 text-xs font-black uppercase text-red-500">Global Network Override</h2>
|
||||
<textarea
|
||||
value={broadcast}
|
||||
onChange={(e) => setBroadcast(e.target.value)}
|
||||
className="mb-4 h-24 w-full resize-none border border-red-900 bg-[#111] p-3 text-xs text-red-500 focus:outline-none"
|
||||
placeholder="Message stored in localStorage key shadow_broadcast…"
|
||||
className="mb-4 h-24 w-full resize-none rounded border border-red-900/50 bg-[#0a0000] p-3 text-xs text-red-400 focus:border-red-500 focus:outline-none"
|
||||
placeholder="Force a message to all users on the network..."
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleBroadcast}
|
||||
className="w-full bg-red-900/90 py-2 text-xs font-bold uppercase text-red-100 transition-all hover:bg-red-800"
|
||||
className="w-full rounded bg-red-900/80 py-3 text-xs font-bold uppercase text-white transition-all hover:bg-red-700 shadow-[0_0_10px_rgba(255,0,0,0.3)]"
|
||||
>
|
||||
Store broadcast (local)
|
||||
EXECUTE OVERRIDE
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="relative h-48 overflow-hidden border border-[#00ff41]/20 bg-black p-6">
|
||||
<h2 className="mb-4 text-xs uppercase opacity-50">Node map</h2>
|
||||
<p className="relative z-10 text-[8px] leading-relaxed opacity-80">
|
||||
Decorative. Real topology is Tor + nginx on your host — see README / DEPLOY.md.
|
||||
</p>
|
||||
<div className="rounded-xl border border-[#00ff66]/20 bg-black p-6">
|
||||
<h2 className="mb-4 text-xs uppercase text-[#00ff66]">Network Operations Logs</h2>
|
||||
<div className="flex h-48 flex-col overflow-hidden rounded bg-[#030005] p-3 text-[9px] text-[#00ff66]/70 border border-white/5">
|
||||
<div className="scrollbar-hide flex-1 space-y-1 overflow-y-auto font-mono">
|
||||
{terminalLines.map((line, i) => (
|
||||
<div key={i} className={line.includes("[ADMIN]") ? "text-red-400 font-bold" : line.includes("SECURITY") ? "text-yellow-400" : ""}>
|
||||
{line}
|
||||
</div>
|
||||
))}
|
||||
<div className="animate-pulse">_</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-span-12 flex flex-col gap-6 lg:col-span-8">
|
||||
<div className="flex min-h-[200px] flex-1 flex-col overflow-hidden border border-[#00ff41]/20 bg-black p-4 text-[10px]">
|
||||
<div className="mb-2 flex items-center justify-between border-b border-[#00ff41]/10 pb-2">
|
||||
<span className="uppercase opacity-50">Chatter console</span>
|
||||
<span className="animate-pulse text-green-500">● FEED</span>
|
||||
<div className="flex flex-1 flex-col overflow-hidden rounded-xl border border-[#00ff66]/30 bg-black/60 p-6">
|
||||
<div className="mb-4 flex items-center justify-between border-b border-[#00ff66]/20 pb-4">
|
||||
<span className="font-bold uppercase text-[#00ff66] tracking-wider text-sm">Comms Interception (Wiretap)</span>
|
||||
<button
|
||||
onClick={clearAllMessages}
|
||||
className="rounded border border-red-900/50 bg-red-900/20 px-4 py-1 text-[10px] font-bold text-red-500 hover:bg-red-900/40"
|
||||
>
|
||||
NUKE ALL
|
||||
</button>
|
||||
</div>
|
||||
<div className="scrollbar-hide flex-1 space-y-1 overflow-y-auto">
|
||||
{terminalLines.map((line, i) => (
|
||||
<div key={`${i}-${line.slice(0, 24)}`} className={line.includes("[ADMIN]") ? "font-bold text-red-500" : ""}>
|
||||
{line}
|
||||
</div>
|
||||
))}
|
||||
<div className="animate-pulse">_</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border border-[#00ff41]/20 bg-black/80 p-4 text-[10px] uppercase tracking-widest opacity-50">
|
||||
<Link href="/dashboard" className="text-[#7dd3fc] hover:underline">
|
||||
Dashboard
|
||||
</Link>
|
||||
{" · "}
|
||||
<Link href="/checkout" className="text-[#7dd3fc] hover:underline">
|
||||
Checkout
|
||||
</Link>
|
||||
{" · "}
|
||||
<Link href="/" className="text-[#7dd3fc] hover:underline">
|
||||
Hub
|
||||
</Link>
|
||||
<div className="scrollbar-hide flex-1 space-y-3 overflow-y-auto pr-2">
|
||||
{messages.length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center text-xs opacity-40">No network traffic detected...</div>
|
||||
) : (
|
||||
messages.map((msg) => (
|
||||
<div key={msg.id} className="flex flex-col gap-1 rounded bg-[#0a110d] p-3 border border-[#00ff66]/10">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[11px] font-bold text-[#00ff66]">@{msg.from}</span>
|
||||
<span className="text-[9px] opacity-50 uppercase bg-black px-1.5 py-0.5 rounded">CH: {msg.channel || "global"}</span>
|
||||
<span className="text-[9px] opacity-40">{new Date(msg.ts).toLocaleString()}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => deleteMessage(msg.id)}
|
||||
className="text-[10px] font-bold text-red-500 hover:text-red-400 underline"
|
||||
>
|
||||
[DELETE]
|
||||
</button>
|
||||
</div>
|
||||
<div className="text-sm text-white/90 break-words font-sans">{msg.text}</div>
|
||||
</div>
|
||||
)).reverse()
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import PayWithUsdBalanceButton from "@/components/PayWithUsdBalanceButton";
|
||||
import ThemedLayout from "@/components/layouts/ThemedLayout";
|
||||
import { useCart } from "@/contexts/CartContext";
|
||||
import type { ShopProduct } from "@/lib/shopCatalog";
|
||||
@@ -164,6 +165,17 @@ export default function DigitalPassesPage() {
|
||||
>
|
||||
{isAdded ? "✓ Added to Cart" : `Add to Cart — $${pass.price}`}
|
||||
</button>
|
||||
<div className="mt-3 border border-[#0f0]/25 bg-black/60 p-3">
|
||||
<p className="mb-2 text-[10px] font-bold uppercase text-[#0f0]/70">Or pay instantly (BTC-funded USD)</p>
|
||||
<PayWithUsdBalanceButton
|
||||
amountUsd={pass.price}
|
||||
title={`Pass: ${pass.name}`}
|
||||
description={`Tier ${pass.tier} · +${pass.luxReward} LUX`}
|
||||
luxBonus={pass.luxReward}
|
||||
size="sm"
|
||||
className="w-full border-[#0f0]/40 !text-[#0f0]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -6,54 +6,13 @@ import { useAccount } from "@/contexts/AccountContext";
|
||||
|
||||
type Msg = {
|
||||
id: string;
|
||||
sender: string;
|
||||
from: string;
|
||||
text: string;
|
||||
time: string;
|
||||
mine: boolean;
|
||||
ts: number;
|
||||
};
|
||||
|
||||
const SEED_MSGS: Omit<Msg, "mine">[] = [
|
||||
{ id: "s1", sender: "void_cartographer", text: "Anyone else notice the latency on the east relay dropped by 40ms this cycle?", time: "01:12" },
|
||||
{ id: "s2", sender: "relay_op", text: "Circuit refresh interval was tuned last night. Should hold for 72h.", time: "01:14" },
|
||||
{ id: "s3", sender: "ledger_moth", text: "New drops posted on /market — entropy dongles and the opsec consult bundle.", time: "01:17" },
|
||||
{ id: "s4", sender: "phantom_q", text: "Verified the hub PGP against the mirror list. All checksums matched.", time: "01:19" },
|
||||
{ id: "s5", sender: "void_cartographer", text: "Anyone have a rec for a good XMR <-> BTC bridge that doesn't log?", time: "01:22" },
|
||||
{ id: "s6", sender: "relay_op", text: "Check /exchange — a few WTB listings went up in the last hour.", time: "01:24" },
|
||||
];
|
||||
|
||||
const AUTO_REPLIES = [
|
||||
"Received. Check your vault for any pending receipts.",
|
||||
"Noted. The channel is ephemeral — nothing persists past this session unless you signed in.",
|
||||
"Copy that. See /forum for threaded discussion on that topic.",
|
||||
"Market is up. Check /drops for the latest scheduled releases.",
|
||||
"Circuit healthy. Reply latency nominal.",
|
||||
"Acknowledged. Keep OPSEC tight and verify mirrors before each session.",
|
||||
];
|
||||
|
||||
const CHAT_KEY = "cyberlux-chat-v1";
|
||||
|
||||
function now() {
|
||||
return new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
||||
}
|
||||
|
||||
function uid() {
|
||||
return Math.random().toString(36).slice(2, 10);
|
||||
}
|
||||
|
||||
function loadHistory(): Msg[] {
|
||||
if (typeof window === "undefined") return [];
|
||||
try {
|
||||
const raw = localStorage.getItem(CHAT_KEY);
|
||||
if (!raw) return [];
|
||||
return JSON.parse(raw) as Msg[];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function saveHistory(msgs: Msg[]) {
|
||||
if (typeof window === "undefined") return;
|
||||
localStorage.setItem(CHAT_KEY, JSON.stringify(msgs.slice(-80)));
|
||||
function now(ts: number) {
|
||||
return new Date(ts).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
||||
}
|
||||
|
||||
export default function ChatWidget() {
|
||||
@@ -65,38 +24,51 @@ export default function ChatWidget() {
|
||||
const [hydrated, setHydrated] = useState(false);
|
||||
const endRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const stored = loadHistory();
|
||||
if (stored.length > 0) {
|
||||
setMessages(stored);
|
||||
} else {
|
||||
const seeded = SEED_MSGS.map((m) => ({ ...m, mine: false }));
|
||||
setMessages(seeded);
|
||||
saveHistory(seeded);
|
||||
const fetchMessages = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch("/api/messages?channel=hub");
|
||||
const data = await res.json();
|
||||
if (data.ok && data.messages) {
|
||||
setMessages(data.messages);
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
setHydrated(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchMessages();
|
||||
setHydrated(true);
|
||||
const interval = setInterval(fetchMessages, 3000);
|
||||
return () => clearInterval(interval);
|
||||
}, [fetchMessages]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hydrated && messages.length > 0) {
|
||||
saveHistory(messages);
|
||||
endRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
}, [messages, hydrated]);
|
||||
|
||||
const send = useCallback(() => {
|
||||
const send = async () => {
|
||||
const text = input.trim();
|
||||
if (!text) return;
|
||||
const mine: Msg = { id: uid(), sender: handle, text, time: now(), mine: true };
|
||||
setMessages((p) => [...p, mine]);
|
||||
setInput("");
|
||||
const delay = 900 + Math.random() * 900;
|
||||
setTimeout(() => {
|
||||
const replyText = AUTO_REPLIES[Math.floor(Math.random() * AUTO_REPLIES.length)]!;
|
||||
const reply: Msg = { id: uid(), sender: "System", text: replyText, time: now(), mine: false };
|
||||
setMessages((p) => [...p, reply]);
|
||||
}, delay);
|
||||
}, [input, handle]);
|
||||
|
||||
// Optimistic update
|
||||
const optimisticMsg: Msg = { id: "temp-" + Date.now(), from: handle, text, ts: Date.now() };
|
||||
setMessages((p) => [...p, optimisticMsg]);
|
||||
|
||||
try {
|
||||
await fetch("/api/messages", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ from: handle, text, channel: "hub" }),
|
||||
});
|
||||
fetchMessages();
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="glass rounded-3xl border border-white/10 p-8">
|
||||
@@ -104,7 +76,7 @@ export default function ChatWidget() {
|
||||
<div>
|
||||
<h2 className="font-orbitron text-3xl font-bold">CHANNEL CHAT</h2>
|
||||
<p className="mt-1 text-sm text-foreground/60">
|
||||
Ephemeral hub channel — stored locally.{" "}
|
||||
Global hub channel — synced live.{" "}
|
||||
{user ? (
|
||||
<span className="text-neon-cyan">Posting as @{user.username}</span>
|
||||
) : (
|
||||
@@ -119,40 +91,47 @@ export default function ChatWidget() {
|
||||
<span className="inline-block h-2 w-2 animate-pulse rounded-full bg-neon-green" />
|
||||
channel active
|
||||
</span>
|
||||
<span>local store</span>
|
||||
<span>live sync</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 h-72 overflow-y-auto rounded-2xl border border-white/10 bg-black/30 p-4 space-y-3">
|
||||
{messages.map((msg) => (
|
||||
<div key={msg.id} className={`flex flex-col ${msg.mine ? "items-end" : "items-start"}`}>
|
||||
<div className="mb-0.5 flex items-center gap-2">
|
||||
<span
|
||||
className={`text-xs font-medium ${
|
||||
msg.mine
|
||||
? "text-neon-cyan"
|
||||
: msg.sender === "System"
|
||||
? "text-neon-purple"
|
||||
: "text-foreground/70"
|
||||
}`}
|
||||
>
|
||||
{msg.mine ? `@${handle}` : msg.sender === "System" ? "⚡ System" : `@${msg.sender}`}
|
||||
</span>
|
||||
<span className="text-[10px] text-foreground/30">{msg.time}</span>
|
||||
</div>
|
||||
<div
|
||||
className={`max-w-[80%] rounded-2xl px-4 py-2 text-sm ${
|
||||
msg.mine
|
||||
? "bg-neon-cyan/15 text-neon-cyan"
|
||||
: msg.sender === "System"
|
||||
? "bg-neon-purple/10 text-neon-purple/90"
|
||||
: "bg-white/5 text-foreground/90"
|
||||
}`}
|
||||
>
|
||||
{msg.text}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{messages.length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center text-sm text-foreground/40">No messages yet. Be the first!</div>
|
||||
) : (
|
||||
messages.map((msg) => {
|
||||
const mine = msg.from === handle || (msg.from === "anon" && !user);
|
||||
return (
|
||||
<div key={msg.id} className={`flex flex-col ${mine ? "items-end" : "items-start"}`}>
|
||||
<div className="mb-0.5 flex items-center gap-2">
|
||||
<span
|
||||
className={`text-xs font-medium ${
|
||||
mine
|
||||
? "text-neon-cyan"
|
||||
: msg.from === "System"
|
||||
? "text-neon-purple"
|
||||
: "text-foreground/70"
|
||||
}`}
|
||||
>
|
||||
{mine ? `@${handle}` : msg.from === "System" ? "⚡ System" : `@${msg.from}`}
|
||||
</span>
|
||||
<span className="text-[10px] text-foreground/30">{now(msg.ts)}</span>
|
||||
</div>
|
||||
<div
|
||||
className={`max-w-[80%] rounded-2xl px-4 py-2 text-sm ${
|
||||
mine
|
||||
? "bg-neon-cyan/15 text-neon-cyan"
|
||||
: msg.from === "System"
|
||||
? "bg-neon-purple/10 text-neon-purple/90"
|
||||
: "bg-white/5 text-foreground/90"
|
||||
}`}
|
||||
>
|
||||
{msg.text}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
<div ref={endRef} />
|
||||
</div>
|
||||
|
||||
@@ -178,7 +157,7 @@ export default function ChatWidget() {
|
||||
|
||||
<div className="mt-6 grid grid-cols-2 gap-4 md:grid-cols-4">
|
||||
{[
|
||||
{ icon: "🔒", label: "Client storage", sub: "Never leaves your browser" },
|
||||
{ icon: "🌍", label: "Global Sync", sub: "Talk to everyone" },
|
||||
{ icon: "💬", label: "Open channel", sub: "Hub-wide thread" },
|
||||
{ icon: "📬", label: "Forum threads", sub: "/forum for persistence" },
|
||||
{ icon: "📦", label: "Market drops", sub: "/drops for schedule" },
|
||||
|
||||
@@ -3,37 +3,46 @@
|
||||
import Link from "next/link";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
|
||||
/** Total time for the progress bar to reach 100% (ms). */
|
||||
const DURATION_MS = 3500;
|
||||
/** Hard cap — always complete after this (ms) even if something glitches. */
|
||||
const MAX_WAIT_MS = 9000;
|
||||
/** Wall-clock polling (Tor Browser throttles requestAnimationFrame aggressively). */
|
||||
const TICK_MS = 50;
|
||||
|
||||
export default function DDoSProtection({ onComplete }: { onComplete: () => void }) {
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [status, setStatus] = useState("Checking your browser…");
|
||||
/** Client-only — avoids SSR/client HTML mismatch from Math.random() in JSX. */
|
||||
const [rayId, setRayId] = useState("");
|
||||
const doneRef = useRef(false);
|
||||
const onCompleteRef = useRef(onComplete);
|
||||
onCompleteRef.current = onComplete;
|
||||
|
||||
const finish = () => {
|
||||
if (doneRef.current) return;
|
||||
doneRef.current = true;
|
||||
setProgress(100);
|
||||
setStatus("Access granted.");
|
||||
window.setTimeout(() => onCompleteRef.current(), 400);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setRayId(Math.random().toString(36).substring(2, 10).toUpperCase());
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setProgress((prev) => {
|
||||
if (prev >= 100) {
|
||||
return 100;
|
||||
}
|
||||
const next = prev + Math.random() * 15;
|
||||
if (next >= 100) {
|
||||
if (!doneRef.current) {
|
||||
doneRef.current = true;
|
||||
clearInterval(interval);
|
||||
setTimeout(() => onCompleteRef.current(), 500);
|
||||
}
|
||||
return 100;
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, 400);
|
||||
const start = Date.now();
|
||||
doneRef.current = false;
|
||||
|
||||
const progressInterval = window.setInterval(() => {
|
||||
const elapsed = Date.now() - start;
|
||||
const pct = Math.min(100, (elapsed / DURATION_MS) * 100);
|
||||
setProgress(pct);
|
||||
if (pct >= 100 || elapsed >= MAX_WAIT_MS) {
|
||||
window.clearInterval(progressInterval);
|
||||
finish();
|
||||
}
|
||||
}, TICK_MS);
|
||||
|
||||
const statusUpdates = [
|
||||
"Checking your browser…",
|
||||
@@ -43,20 +52,24 @@ export default function DDoSProtection({ onComplete }: { onComplete: () => void
|
||||
"Establishing encrypted tunnel…",
|
||||
"Access granted.",
|
||||
];
|
||||
|
||||
let statusIndex = 0;
|
||||
const statusInterval = setInterval(() => {
|
||||
const statusInterval = window.setInterval(() => {
|
||||
if (statusIndex < statusUpdates.length - 1) {
|
||||
statusIndex++;
|
||||
setStatus(statusUpdates[statusIndex]);
|
||||
setStatus(statusUpdates[statusIndex]!);
|
||||
} else {
|
||||
clearInterval(statusInterval);
|
||||
}
|
||||
}, 800);
|
||||
}, 650);
|
||||
|
||||
const maxTimer = window.setTimeout(() => {
|
||||
if (!doneRef.current) finish();
|
||||
}, MAX_WAIT_MS);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
window.clearInterval(progressInterval);
|
||||
clearInterval(statusInterval);
|
||||
clearTimeout(maxTimer);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -69,25 +82,36 @@ export default function DDoSProtection({ onComplete }: { onComplete: () => void
|
||||
className="w-full max-w-md border-2 border-[#00ff41]/20 bg-[#050505] p-12 shadow-[0_0_50px_rgba(0,255,65,0.1)]"
|
||||
style={{ backgroundColor: "#050505", borderColor: "rgba(0,255,65,0.2)" }}
|
||||
>
|
||||
<div className="text-4xl mb-8 animate-pulse">🛡️</div>
|
||||
<h1 className="text-2xl font-bold mb-2 uppercase tracking-tighter">DDoS Protection</h1>
|
||||
<p className="text-xs opacity-60 mb-8 leading-relaxed">
|
||||
<div className="mb-8 animate-pulse text-4xl">🛡️</div>
|
||||
<h1 className="mb-2 text-2xl font-bold uppercase tracking-tighter">DDoS Protection</h1>
|
||||
<p className="mb-8 text-xs leading-relaxed opacity-60">
|
||||
Origin shield active — wait while we validate your session. Automated requests are throttled at the edge.
|
||||
</p>
|
||||
|
||||
<div className="w-full h-2 bg-[#00ff41]/10 mb-4 overflow-hidden">
|
||||
<div className="mb-4 h-2 w-full overflow-hidden bg-[#00ff41]/10">
|
||||
<div
|
||||
className="h-full bg-[#00ff41] transition-all duration-300 ease-out"
|
||||
style={{ width: `${progress}%` }}
|
||||
className="h-full bg-[#00ff41] transition-[width] duration-100 ease-linear"
|
||||
style={{ width: `${Math.min(100, progress)}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between text-[10px] uppercase tracking-widest">
|
||||
<span>{status}</span>
|
||||
<span>{Math.floor(progress)}%</span>
|
||||
<span>{Math.floor(Math.min(100, progress))}%</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 text-[8px] opacity-20 leading-relaxed">
|
||||
<button
|
||||
type="button"
|
||||
onClick={finish}
|
||||
className="mt-8 w-full border border-[#00ff41]/40 bg-[#00ff41]/10 py-3 text-xs font-bold uppercase tracking-wider text-[#00ff41] transition hover:bg-[#00ff41]/20"
|
||||
>
|
||||
Continue to site
|
||||
</button>
|
||||
<p className="mt-2 text-center text-[9px] text-[#00ff41]/40">
|
||||
Stuck on Tor or slow device? Tap above — no challenge required.
|
||||
</p>
|
||||
|
||||
<div className="mt-8 text-[8px] leading-relaxed opacity-20">
|
||||
Ray ID: {rayId || "—"}
|
||||
<br />
|
||||
Performance & Security by ShadowGuard™
|
||||
|
||||
@@ -32,7 +32,7 @@ const Hero = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className="relative min-h-screen overflow-hidden px-4 pt-32 md:pt-40">
|
||||
<section className="relative min-h-[85vh] overflow-hidden px-4 pt-24 md:min-h-screen md:pt-28">
|
||||
{/* Animated background particles */}
|
||||
<div className="absolute inset-0 -z-20">
|
||||
{particles.map((p, i) => (
|
||||
@@ -69,25 +69,25 @@ const Hero = () => {
|
||||
|
||||
<div className="container relative mx-auto max-w-6xl">
|
||||
{/* Glitch text effect */}
|
||||
<div className="relative mb-6">
|
||||
<h1 className="font-orbitron text-5xl font-black leading-tight text-foreground md:text-7xl lg:text-8xl">
|
||||
<span className="block">REVOLUTIONARY</span>
|
||||
<div className="relative mb-5">
|
||||
<h1 className="font-orbitron text-3xl font-black leading-[1.1] text-foreground sm:text-4xl md:text-5xl lg:text-6xl">
|
||||
<span className="block">FORBIDDEN</span>
|
||||
<span className="block text-transparent bg-gradient-to-r from-neon-cyan via-neon-purple to-neon-pink bg-clip-text">
|
||||
E‑COMMERCE
|
||||
DARK ARTS
|
||||
</span>
|
||||
<span className="block">EXPERIENCE</span>
|
||||
<span className="block">EMPORIUM</span>
|
||||
</h1>
|
||||
<div className="absolute -top-2 left-0 -z-10 text-5xl font-black text-neon-cyan opacity-30 blur-md md:text-7xl lg:text-8xl">
|
||||
REVOLUTIONARY
|
||||
<div className="pointer-events-none absolute -top-1 left-0 -z-10 text-3xl font-black text-neon-cyan opacity-20 blur-sm sm:text-4xl md:text-5xl lg:text-6xl">
|
||||
FORBIDDEN
|
||||
</div>
|
||||
<div className="absolute -bottom-2 right-0 -z-10 text-5xl font-black text-neon-purple opacity-30 blur-md md:text-7xl lg:text-8xl">
|
||||
EXPERIENCE
|
||||
<div className="pointer-events-none absolute -bottom-1 right-0 -z-10 text-3xl font-black text-neon-purple opacity-20 blur-sm sm:text-4xl md:text-5xl lg:text-6xl">
|
||||
EMPORIUM
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="mb-8 max-w-2xl text-xl text-foreground/80 md:text-2xl">
|
||||
Curated <span className="text-neon-cyan">gray-market candy</span> storefront with live catalog, vendor hall, and BTC-settled
|
||||
checkout — plus companion onions for forums, listings, and the wiki rail.
|
||||
<p className="mb-6 max-w-2xl text-base text-foreground/80 md:text-lg">
|
||||
Curated <span className="text-neon-cyan">mystical artifact</span> apothecary with live catalog, alchemist guilds, and soul-bound
|
||||
checkout — plus companion realms for covenants, incantations, and the forbidden wiki.
|
||||
</p>
|
||||
|
||||
<div className="mb-10 flex flex-wrap gap-2 font-mono text-[10px] uppercase tracking-[0.2em] text-foreground/45">
|
||||
@@ -143,18 +143,18 @@ const Hero = () => {
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="mt-20 grid grid-cols-2 gap-6 sm:grid-cols-4">
|
||||
<div className="mt-12 grid grid-cols-2 gap-4 sm:mt-16 sm:grid-cols-4">
|
||||
{[
|
||||
{ value: String(SHOP_PRODUCT_COUNT), label: "Live SKUs", color: "text-neon-cyan" },
|
||||
{ value: String(SHOP_SELLER_COUNT), label: "Vendor stalls", color: "text-neon-green" },
|
||||
{ value: "₿ · Ξ · XMR", label: "Settlement rails", color: "text-neon-purple" },
|
||||
{ value: "v3 ring", label: "Tor-ready layout", color: "text-neon-pink" },
|
||||
{ value: String(SHOP_PRODUCT_COUNT), label: "Arcane Relics", color: "text-neon-cyan" },
|
||||
{ value: String(SHOP_SELLER_COUNT), label: "Alchemist Guilds", color: "text-neon-green" },
|
||||
{ value: "Mana · Aether", label: "Soul bindings", color: "text-neon-purple" },
|
||||
{ value: "v3 coven", label: "Shadow routing", color: "text-neon-pink" },
|
||||
].map((stat) => (
|
||||
<div
|
||||
key={stat.label}
|
||||
className="glass rounded-2xl border border-white/10 p-6 backdrop-blur-sm"
|
||||
>
|
||||
<div className={`font-orbitron text-3xl font-bold ${stat.color}`}>{stat.value}</div>
|
||||
<div className={`font-orbitron text-xl font-bold sm:text-2xl ${stat.color}`}>{stat.value}</div>
|
||||
<div className="mt-2 text-sm text-foreground/60">{stat.label}</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -1,98 +1,134 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { useWallet } from "@/contexts/WalletContext";
|
||||
import { useAccount } from "@/contexts/AccountContext";
|
||||
import { useCart } from "@/contexts/CartContext";
|
||||
import { SITE_NAV_GROUPS } from "@/lib/siteNav";
|
||||
|
||||
const Navbar = () => {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const [pagesOpen, setPagesOpen] = useState(false);
|
||||
const pagesDropdownRef = useRef<HTMLDivElement>(null);
|
||||
const { luxCredits, usdStoreCredit } = useWallet();
|
||||
const { user, hydrated } = useAccount();
|
||||
const { itemCount, hydrated: cartReady } = useCart();
|
||||
|
||||
const navItems = [
|
||||
{ label: "Sanctuary", href: "/sanctuary", icon: "🛡️" },
|
||||
{ label: "Market", href: "/market", icon: "📈" },
|
||||
{ label: "Vendors", href: "/vendors", icon: "🏪" },
|
||||
{ label: "Checkout", href: "/checkout", icon: "₿" },
|
||||
{ label: "Forum", href: "/forum", icon: "◆" },
|
||||
{ label: "Exchange", href: "/exchange", icon: "📰" },
|
||||
{ label: "Barter", href: "/barter", icon: "♻️" },
|
||||
{ label: "Vault", href: "/vault", icon: "🗝️" },
|
||||
{ label: "Wiki", href: "/hidden-wiki", icon: "📚" },
|
||||
{ label: "Atlas", href: "/darknet-atlas", icon: "🗺️" },
|
||||
{ label: "Syndicate", href: "/syndicate", icon: "🕸️" },
|
||||
{ label: "Void crawl", href: "/search", icon: "🔦" },
|
||||
{ label: "Mirrors", href: "/account/hidden-services", icon: "🧅" },
|
||||
{ label: "Add funds", href: "/account/add-funds", icon: "₿" },
|
||||
{ label: "Dark web launch", href: "/launch", icon: "🔥" },
|
||||
];
|
||||
useEffect(() => {
|
||||
const onDoc = (e: MouseEvent) => {
|
||||
if (!pagesDropdownRef.current?.contains(e.target as Node)) setPagesOpen(false);
|
||||
};
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") setPagesOpen(false);
|
||||
};
|
||||
document.addEventListener("mousedown", onDoc);
|
||||
document.addEventListener("keydown", onKey);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", onDoc);
|
||||
document.removeEventListener("keydown", onKey);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<nav className="glass fixed top-0 left-0 right-0 z-50 mx-auto mt-4 max-w-7xl rounded-2xl border border-white/10 px-6 py-4 backdrop-blur-xl">
|
||||
<div className="flex items-center justify-between">
|
||||
{/* Logo */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative">
|
||||
<div className="h-10 w-10 rounded-full bg-gradient-to-br from-neon-cyan to-neon-purple p-0.5">
|
||||
<div className="h-full w-full rounded-full bg-background flex items-center justify-center">
|
||||
<span className="text-xl font-bold">⏣</span>
|
||||
<nav className="glass fixed top-0 left-0 right-0 z-50 mx-auto mt-2 max-w-7xl rounded-xl border border-white/10 px-4 py-2 backdrop-blur-xl md:px-5">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2 md:gap-3">
|
||||
<Link href="/" className="flex shrink-0 items-center gap-2">
|
||||
<div className="relative h-8 w-8 shrink-0 rounded-full bg-gradient-to-br from-neon-cyan to-neon-purple p-0.5">
|
||||
<div className="flex h-full w-full items-center justify-center rounded-full bg-background">
|
||||
<span className="text-sm font-bold">⏣</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute -inset-1 -z-10 animate-pulse rounded-full bg-neon-cyan blur-md opacity-30"></div>
|
||||
</div>
|
||||
<Link href="/" className="font-orbitron text-2xl font-bold tracking-tighter">
|
||||
Cyber<span className="text-neon-cyan">Lux</span>
|
||||
<span className="font-orbitron truncate text-base font-bold tracking-tight md:text-lg">
|
||||
Eldritch<span className="text-neon-cyan">Weave</span>
|
||||
</span>
|
||||
</Link>
|
||||
<span className="hidden rounded-full bg-muted px-3 py-1 text-xs font-medium text-neon-green sm:inline">
|
||||
v1.0 • ALPHA
|
||||
<span className="hidden rounded-full bg-muted/80 px-2 py-0.5 text-[10px] font-medium text-neon-green/90 sm:inline">
|
||||
v1 · spellcraft
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<div className="hidden max-w-lg flex-wrap items-center justify-end gap-x-3 gap-y-1 md:flex lg:max-w-2xl lg:gap-x-4 xl:max-w-none xl:flex-nowrap xl:gap-x-5">
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.label}
|
||||
href={item.href}
|
||||
className="group relative text-sm font-medium text-foreground/80 transition-colors hover:text-neon-cyan"
|
||||
<div ref={pagesDropdownRef} className="relative ml-1 hidden md:block">
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={pagesOpen}
|
||||
aria-haspopup="true"
|
||||
onClick={() => setPagesOpen((o) => !o)}
|
||||
className="flex items-center gap-1 rounded-lg border border-white/15 bg-white/5 px-3 py-1.5 text-xs font-semibold text-foreground/85 transition hover:border-neon-cyan/35 hover:text-neon-cyan"
|
||||
>
|
||||
<span className="mr-1 opacity-60">{item.icon}</span>
|
||||
{item.label}
|
||||
<span className="absolute -bottom-1 left-0 h-0.5 w-0 bg-gradient-to-r from-neon-cyan to-neon-purple transition-all group-hover:w-full"></span>
|
||||
</Link>
|
||||
))}
|
||||
All pages
|
||||
<span className="text-[10px] opacity-70">{pagesOpen ? "▴" : "▾"}</span>
|
||||
</button>
|
||||
{pagesOpen ? (
|
||||
<div className="absolute left-0 top-full z-[60] mt-1 max-h-[min(70vh,520px)] w-[min(100vw-2rem,380px)] overflow-y-auto rounded-xl border border-white/15 bg-[#0a0a0f]/95 p-3 shadow-2xl shadow-black/50 backdrop-blur-xl">
|
||||
{SITE_NAV_GROUPS.map((group) => (
|
||||
<div key={group.title} className="mb-3 last:mb-0">
|
||||
<p className="mb-1.5 px-1 font-mono text-[9px] font-bold uppercase tracking-[0.2em] text-foreground/40">
|
||||
{group.title}
|
||||
</p>
|
||||
<ul className="space-y-0.5">
|
||||
{group.items.map((item) => (
|
||||
<li key={item.href}>
|
||||
<Link
|
||||
href={item.href}
|
||||
className="flex items-center gap-2 rounded-lg px-2 py-1.5 text-xs text-foreground/80 transition hover:bg-white/10 hover:text-neon-cyan"
|
||||
onClick={() => setPagesOpen(false)}
|
||||
>
|
||||
<span className="w-5 text-center opacity-70">{item.icon}</span>
|
||||
<span className="truncate">{item.label}</span>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href="/market"
|
||||
className="hidden text-xs font-medium text-foreground/70 transition hover:text-neon-cyan lg:inline"
|
||||
>
|
||||
Market
|
||||
</Link>
|
||||
<Link
|
||||
href="/checkout"
|
||||
className="hidden text-xs font-medium text-foreground/70 transition hover:text-neon-purple lg:inline"
|
||||
>
|
||||
Checkout
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-3 md:gap-4">
|
||||
<div className="flex shrink-0 items-center gap-2 md:gap-3">
|
||||
{cartReady ? (
|
||||
<Link
|
||||
href="/checkout"
|
||||
className="relative hidden rounded-full border border-white/10 px-3 py-2 text-sm text-foreground/80 hover:border-neon-cyan/40 hover:text-neon-cyan sm:inline-flex"
|
||||
className="relative inline-flex rounded-full border border-white/10 px-2.5 py-1.5 text-xs text-foreground/80 hover:border-neon-cyan/40 hover:text-neon-cyan"
|
||||
title="Cart"
|
||||
>
|
||||
🛒
|
||||
{itemCount > 0 ? (
|
||||
<span className="absolute -right-1 -top-1 flex h-5 min-w-5 items-center justify-center rounded-full bg-neon-pink px-1 text-[10px] font-bold text-background">
|
||||
<span className="absolute -right-1 -top-1 flex h-4 min-w-4 items-center justify-center rounded-full bg-neon-pink px-0.5 text-[9px] font-bold text-background">
|
||||
{itemCount > 99 ? "99+" : itemCount}
|
||||
</span>
|
||||
) : null}
|
||||
</Link>
|
||||
) : null}
|
||||
<div className="hidden md:flex items-center gap-3">
|
||||
<div className="flex items-center gap-2 rounded-full bg-white/5 px-4 py-2 border border-white/10">
|
||||
<span className="text-xs text-foreground/60">LUX</span>
|
||||
<span className="font-orbitron text-sm text-neon-green">{luxCredits.toLocaleString()}</span>
|
||||
<div className="hidden items-center gap-1.5 sm:flex">
|
||||
<div
|
||||
className="flex items-center gap-1 rounded-full border border-white/10 bg-white/5 px-2 py-1 text-[10px]"
|
||||
title="LUX credits"
|
||||
>
|
||||
<span className="text-foreground/50">LUX</span>
|
||||
<span className="font-orbitron text-[11px] text-neon-green">{luxCredits.toLocaleString()}</span>
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center gap-2 rounded-full bg-white/5 px-4 py-2 border border-neon-cyan/20"
|
||||
title="Verified Bitcoin deposit balance (USD)"
|
||||
className="flex items-center gap-1 rounded-full border border-neon-cyan/20 bg-white/5 px-2 py-1 text-[10px]"
|
||||
title="Bitcoin-funded USD balance"
|
||||
>
|
||||
<span className="text-xs text-foreground/60">USD</span>
|
||||
<span className="font-orbitron text-sm text-neon-cyan">${usdStoreCredit.toFixed(2)}</span>
|
||||
<span className="text-foreground/50">$</span>
|
||||
<span className="font-orbitron text-[11px] text-neon-cyan">{usdStoreCredit.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
{hydrated ? (
|
||||
@@ -100,28 +136,28 @@ const Navbar = () => {
|
||||
<>
|
||||
<Link
|
||||
href="/account/add-funds"
|
||||
className="hidden rounded-full border border-neon-green/35 bg-neon-green/10 px-3 py-2 text-xs font-bold uppercase tracking-wide text-neon-green hover:bg-neon-green/15 sm:inline-flex"
|
||||
className="hidden rounded-full border border-neon-green/30 bg-neon-green/10 px-2.5 py-1 text-[10px] font-bold uppercase tracking-wide text-neon-green hover:bg-neon-green/15 sm:inline-flex"
|
||||
>
|
||||
Add funds
|
||||
+Funds
|
||||
</Link>
|
||||
<Link
|
||||
href="/dashboard"
|
||||
className="hidden items-center gap-2 rounded-full border border-neon-cyan/30 bg-neon-cyan/10 px-4 py-2 text-sm font-medium text-neon-cyan hover:bg-neon-cyan/20 sm:flex"
|
||||
className="hidden max-w-[6rem] truncate rounded-full border border-neon-cyan/25 bg-neon-cyan/10 px-2.5 py-1 text-[11px] font-medium text-neon-cyan hover:bg-neon-cyan/18 sm:inline"
|
||||
>
|
||||
<span className="max-w-[8rem] truncate">{user.displayName}</span>
|
||||
{user.displayName}
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Link
|
||||
href="/sign-in"
|
||||
className="hidden rounded-full border border-white/15 px-4 py-2 text-sm font-medium text-foreground/80 hover:border-neon-cyan/50 hover:text-neon-cyan sm:inline-flex"
|
||||
className="hidden rounded-full border border-white/12 px-2.5 py-1 text-[11px] font-medium text-foreground/80 hover:border-neon-cyan/45 hover:text-neon-cyan sm:inline-flex"
|
||||
>
|
||||
Sign in
|
||||
</Link>
|
||||
<Link
|
||||
href="/sign-up"
|
||||
className="hidden rounded-full border border-neon-purple/35 bg-neon-purple/10 px-4 py-2 text-sm font-medium text-neon-purple hover:bg-neon-purple/20 md:inline-flex"
|
||||
className="hidden rounded-full border border-neon-purple/30 bg-neon-purple/10 px-2.5 py-1 text-[11px] font-medium text-neon-purple hover:bg-neon-purple/18 md:inline-flex"
|
||||
>
|
||||
Register
|
||||
</Link>
|
||||
@@ -129,41 +165,39 @@ const Navbar = () => {
|
||||
)
|
||||
) : null}
|
||||
<button
|
||||
className="rounded-full bg-white/5 p-2 hover:bg-white/10"
|
||||
className="rounded-full bg-white/5 p-1.5 hover:bg-white/10 md:hidden"
|
||||
aria-label="Toggle menu"
|
||||
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
||||
>
|
||||
<div className="h-5 w-5 space-y-1.5">
|
||||
<div className={`h-0.5 bg-neon-cyan transition-transform ${isMenuOpen ? "translate-y-2 rotate-45" : ""}`}></div>
|
||||
<div className={`h-0.5 bg-neon-purple transition-opacity ${isMenuOpen ? "opacity-0" : ""}`}></div>
|
||||
<div className={`h-0.5 bg-neon-pink transition-transform ${isMenuOpen ? "-translate-y-2 -rotate-45" : ""}`}></div>
|
||||
<div className="h-4 w-4 space-y-1">
|
||||
<div className={`h-0.5 bg-neon-cyan transition-transform ${isMenuOpen ? "translate-y-1.5 rotate-45" : ""}`} />
|
||||
<div className={`h-0.5 bg-neon-purple transition-opacity ${isMenuOpen ? "opacity-0" : ""}`} />
|
||||
<div className={`h-0.5 bg-neon-pink transition-transform ${isMenuOpen ? "-translate-y-1.5 -rotate-45" : ""}`} />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu */}
|
||||
{/* Mobile: full menu + page list */}
|
||||
{isMenuOpen && (
|
||||
<div className="glass mt-4 rounded-xl border border-white/10 p-4 backdrop-blur-xl md:hidden">
|
||||
<div className="mb-3 flex gap-2 border-b border-white/10 pb-3">
|
||||
<div className="glass mt-2 max-h-[75vh] overflow-y-auto rounded-lg border border-white/10 p-3 backdrop-blur-xl md:hidden">
|
||||
<div className="mb-2 flex flex-wrap gap-2 border-b border-white/10 pb-2">
|
||||
{cartReady ? (
|
||||
<Link
|
||||
href="/checkout"
|
||||
className="relative flex-1 rounded-lg border border-white/15 py-2 text-center text-sm font-medium"
|
||||
className="relative flex-1 rounded-lg border border-white/15 py-2 text-center text-xs font-medium"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
🛒 Cart
|
||||
{itemCount > 0 ? (
|
||||
<span className="ml-1 rounded bg-neon-pink px-1.5 text-[10px] font-bold text-background">
|
||||
{itemCount}
|
||||
</span>
|
||||
<span className="ml-1 rounded bg-neon-pink px-1 text-[9px] font-bold text-background">{itemCount}</span>
|
||||
) : null}
|
||||
</Link>
|
||||
) : null}
|
||||
{hydrated && user ? (
|
||||
<Link
|
||||
href="/dashboard"
|
||||
className="flex-1 rounded-lg bg-neon-cyan/15 py-2 text-center text-sm font-bold text-neon-cyan"
|
||||
className="flex-1 rounded-lg bg-neon-cyan/15 py-2 text-center text-xs font-bold text-neon-cyan"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
Dashboard
|
||||
@@ -171,51 +205,31 @@ const Navbar = () => {
|
||||
) : hydrated ? (
|
||||
<Link
|
||||
href="/sign-in"
|
||||
className="flex-1 rounded-lg border border-white/15 py-2 text-center text-sm font-medium"
|
||||
className="flex-1 rounded-lg border border-white/15 py-2 text-center text-xs font-medium"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
Sign in
|
||||
</Link>
|
||||
) : null}
|
||||
{hydrated && !user ? (
|
||||
<Link
|
||||
href="/sign-up"
|
||||
className="flex-1 rounded-lg border border-neon-purple/40 py-2 text-center text-sm text-neon-purple"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
Register
|
||||
</Link>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.label}
|
||||
href={item.href}
|
||||
className="flex items-center gap-2 rounded-lg bg-white/5 p-3 text-sm font-medium transition-colors hover:bg-white/10"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
<span className="text-lg">{item.icon}</span>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-4 flex gap-3 border-t border-white/10 pt-4 text-xs">
|
||||
<Link
|
||||
href="/sanctuary"
|
||||
className="flex-1 rounded-lg border border-white/10 px-3 py-2 text-center text-foreground/70 hover:border-neon-cyan/40 hover:text-neon-cyan"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
Sanctuary
|
||||
</Link>
|
||||
<Link
|
||||
href="/account/hidden-services"
|
||||
className="flex-1 rounded-lg border border-white/10 px-3 py-2 text-center text-foreground/70 hover:border-neon-purple/40 hover:text-neon-purple"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
Onion map
|
||||
</Link>
|
||||
</div>
|
||||
{SITE_NAV_GROUPS.map((group) => (
|
||||
<div key={group.title} className="mb-3">
|
||||
<p className="mb-1 font-mono text-[9px] font-bold uppercase tracking-widest text-foreground/35">{group.title}</p>
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
{group.items.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="flex items-center gap-1.5 rounded-md bg-white/5 p-2 text-[11px] font-medium"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
<span>{item.icon}</span>
|
||||
<span className="truncate">{item.label}</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
111
components/PayWithUsdBalanceButton.tsx
Normal file
111
components/PayWithUsdBalanceButton.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useAccount } from "@/contexts/AccountContext";
|
||||
import { useWallet } from "@/contexts/WalletContext";
|
||||
|
||||
type Props = {
|
||||
/** USD amount to charge (2 decimals) */
|
||||
amountUsd: number;
|
||||
/** Receipt / vault title */
|
||||
title: string;
|
||||
description?: string;
|
||||
className?: string;
|
||||
size?: "sm" | "md";
|
||||
/** Override LUX granted (default: based on USD amount) */
|
||||
luxBonus?: number;
|
||||
/** Called after a successful charge */
|
||||
onSuccess?: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Spend verified Bitcoin-funded USD balance without going through full checkout cart.
|
||||
*/
|
||||
export default function PayWithUsdBalanceButton({
|
||||
amountUsd,
|
||||
title,
|
||||
description,
|
||||
className = "",
|
||||
size = "md",
|
||||
luxBonus,
|
||||
onSuccess,
|
||||
}: Props) {
|
||||
const { user } = useAccount();
|
||||
const { usdStoreCredit, spendUsdStoreCredit, earnLuxCredits, addVaultReceipt } = useWallet();
|
||||
const [msg, setMsg] = useState<{ kind: "ok" | "err"; text: string } | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const safe = Math.round(amountUsd * 100) / 100;
|
||||
const pad = size === "sm" ? "px-3 py-1.5 text-xs" : "px-5 py-2.5 text-sm";
|
||||
|
||||
const pay = () => {
|
||||
setMsg(null);
|
||||
if (!user) {
|
||||
setMsg({ kind: "err", text: "Sign in to spend your USD balance." });
|
||||
return;
|
||||
}
|
||||
if (!(safe > 0)) {
|
||||
setMsg({ kind: "err", text: "Invalid amount." });
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
try {
|
||||
if (!spendUsdStoreCredit(safe)) {
|
||||
setMsg({
|
||||
kind: "err",
|
||||
text: `Need $${safe.toFixed(2)} — you have $${usdStoreCredit.toFixed(2)}. Add funds first.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const lux =
|
||||
luxBonus != null && Number.isFinite(luxBonus)
|
||||
? Math.max(0, Math.floor(luxBonus))
|
||||
: Math.min(500, Math.max(10, Math.floor(safe)));
|
||||
earnLuxCredits(lux);
|
||||
const oid =
|
||||
typeof crypto !== "undefined" && "randomUUID" in crypto
|
||||
? `PAY-${crypto.randomUUID().slice(0, 8).toUpperCase()}`
|
||||
: `PAY-${Date.now().toString(36).toUpperCase()}`;
|
||||
addVaultReceipt({
|
||||
id: oid,
|
||||
title,
|
||||
desc: description ?? `${title} · $${safe.toFixed(2)} USD`,
|
||||
date: new Date().toISOString().slice(0, 10),
|
||||
severity: "normal",
|
||||
});
|
||||
setMsg({ kind: "ok", text: `Paid $${safe.toFixed(2)}. +${lux} LUX · receipt ${oid}` });
|
||||
onSuccess?.();
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy || safe <= 0}
|
||||
onClick={pay}
|
||||
className={`rounded-full border border-neon-cyan/40 bg-neon-cyan/15 font-bold text-neon-cyan transition hover:bg-neon-cyan/25 disabled:opacity-40 ${pad} ${className}`}
|
||||
>
|
||||
{busy ? "Processing…" : `Pay $${safe.toFixed(2)} with balance`}
|
||||
</button>
|
||||
{!user ? (
|
||||
<p className="text-xs text-foreground/55">
|
||||
<Link href="/sign-in" className="text-neon-cyan underline">
|
||||
Sign in
|
||||
</Link>{" "}
|
||||
to use BTC-funded USD credit.
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-xs text-foreground/45">
|
||||
Balance: <span className="font-orbitron text-neon-green">${usdStoreCredit.toFixed(2)}</span>
|
||||
</p>
|
||||
)}
|
||||
{msg ? (
|
||||
<p className={`text-xs ${msg.kind === "ok" ? "text-neon-green" : "text-red-400"}`}>{msg.text}</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
46
components/market/MarketCommerceBar.tsx
Normal file
46
components/market/MarketCommerceBar.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useWallet } from "@/contexts/WalletContext";
|
||||
import { useCart } from "@/contexts/CartContext";
|
||||
import { useAccount } from "@/contexts/AccountContext";
|
||||
|
||||
export default function MarketCommerceBar() {
|
||||
const { usdStoreCredit, luxCredits } = useWallet();
|
||||
const { itemCount, hydrated } = useCart();
|
||||
const { user, hydrated: accReady } = useAccount();
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center justify-end gap-2 border-b border-[#00ff41]/15 bg-[#040805]/90 px-3 py-2 font-mono text-[10px] text-[#8fccb0]">
|
||||
<span className="text-[#5a8f6a]">Spendable</span>
|
||||
<span title="Bitcoin-funded USD">
|
||||
USD <strong className="text-[#7af598]">${usdStoreCredit.toFixed(2)}</strong>
|
||||
</span>
|
||||
<span className="text-[#00ff41]/25">|</span>
|
||||
<span title="LUX credits">
|
||||
LUX <strong className="text-[#b4ffcc]">{luxCredits.toLocaleString()}</strong>
|
||||
</span>
|
||||
{accReady && !user ? (
|
||||
<>
|
||||
<span className="text-[#00ff41]/25">|</span>
|
||||
<Link href="/sign-in?next=/market" className="text-[#7af598] underline hover:text-white">
|
||||
Sign in to pay
|
||||
</Link>
|
||||
</>
|
||||
) : null}
|
||||
<span className="text-[#00ff41]/25">|</span>
|
||||
<Link
|
||||
href="/checkout"
|
||||
className="inline-flex items-center gap-1 rounded border border-[#00ff41]/35 px-2 py-1 text-[#c8ffd8] hover:border-[#00ff41]/60"
|
||||
>
|
||||
🛒 Checkout
|
||||
{hydrated && itemCount > 0 ? (
|
||||
<span className="rounded bg-[#ff00aa]/90 px-1 text-[9px] font-bold text-black">{itemCount}</span>
|
||||
) : null}
|
||||
</Link>
|
||||
<Link href="/account/add-funds" className="rounded border border-[#00ff41]/20 px-2 py-1 hover:border-[#00ff41]/45">
|
||||
+Funds
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import MarketCommerceBar from "@/components/market/MarketCommerceBar";
|
||||
import LexiconStrip from "@/components/site/LexiconStrip";
|
||||
import { MARKET_ATMOSPHERE_TAGS } from "@/lib/cyberluxZoneLexicon";
|
||||
import { SHOP_PRODUCT_COUNT } from "@/lib/shopCatalog";
|
||||
@@ -70,15 +71,18 @@ export default function MarketSiteChrome({ children }: { children: React.ReactNo
|
||||
/>
|
||||
|
||||
<header className="sticky top-0 z-30 border-b border-[#00ff41]/20 bg-[#030806]/95 backdrop-blur-md">
|
||||
<div className="mx-auto flex max-w-[1600px] flex-col gap-3 px-4 py-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div className="mx-auto max-w-[1600px]">
|
||||
<MarketCommerceBar />
|
||||
</div>
|
||||
<div className="mx-auto flex max-w-[1600px] flex-col gap-3 px-4 py-3 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<p className="font-mono text-[9px] uppercase tracking-[0.5em] text-[#00ff41]/45">sovereign catalog · multi-layer</p>
|
||||
<Link href="/market" className="mt-1 block">
|
||||
<h1 className="font-mono text-2xl font-black tracking-tight text-[#7af598] md:text-3xl">
|
||||
<h1 className="font-mono text-xl font-black tracking-tight text-[#7af598] md:text-2xl">
|
||||
The Candy Shop
|
||||
</h1>
|
||||
</Link>
|
||||
<p className="mt-1 max-w-xl font-mono text-[11px] text-[#00ff41]/55">
|
||||
<p className="mt-1 max-w-xl font-mono text-[10px] text-[#00ff41]/55 md:text-[11px]">
|
||||
<strong className="text-[#9dffc4]">{SHOP_PRODUCT_COUNT}</strong> SKUs ·{" "}
|
||||
<strong className="text-[#9dffc4]">{SHOP_SELLER_COUNT}</strong> stalls — not a plugin mall; one cart, one checkout, four trust layers.
|
||||
</p>
|
||||
|
||||
@@ -21,6 +21,7 @@ export type PublicCredentials = {
|
||||
username: string;
|
||||
displayName: string;
|
||||
createdAt: number;
|
||||
isAdmin?: boolean;
|
||||
};
|
||||
|
||||
function accountKey(username: string): string {
|
||||
@@ -94,6 +95,23 @@ export async function verifyCredentials(
|
||||
password: string,
|
||||
): Promise<{ ok: true; profile: PublicCredentials } | { ok: false; error: string }> {
|
||||
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 row = map[key];
|
||||
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." };
|
||||
return {
|
||||
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 row = map[key];
|
||||
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 {
|
||||
|
||||
34
lib/cyberluxCrossNav.ts
Normal file
34
lib/cyberluxCrossNav.ts
Normal 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
19
lib/cyberluxSitemap.ts
Normal 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: "🚀" },
|
||||
];
|
||||
@@ -103,7 +103,7 @@ function block(category: string, items: Seed[]): ProductDraft[] {
|
||||
|
||||
/** Everything offered in the shop — expand by adding more `block(...)` rows */
|
||||
const ALL_BLOCKS: ProductDraft[] = [
|
||||
...block("Bio‑Enhancements", [
|
||||
...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: "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" },
|
||||
@@ -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: "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‑Forged 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" },
|
||||
@@ -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: "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: "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" },
|
||||
@@ -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: "Forged signature practice pad", description: "Legal-only handwriting exercise pad — anti-fraud angle.", price: 17, currency: "USD" },
|
||||
]),
|
||||
...block("Security", [
|
||||
...block("Ward Enchantments", [
|
||||
{ name: "Zero‑Day 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: "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: "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: "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" },
|
||||
@@ -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: "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: "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" },
|
||||
@@ -199,7 +199,7 @@ const ALL_BLOCKS: ProductDraft[] = [
|
||||
{ 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" },
|
||||
]),
|
||||
...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: "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" },
|
||||
@@ -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: "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: "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" },
|
||||
@@ -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: "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: "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 },
|
||||
@@ -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: "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: "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" },
|
||||
@@ -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: "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: "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" },
|
||||
@@ -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: "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: "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" },
|
||||
@@ -304,20 +304,24 @@ export function shopProductsForSeller(sellerId: string): ShopProduct[] {
|
||||
|
||||
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 }[] {
|
||||
const icons: Record<string, string> = {
|
||||
"Bio‑Enhancements": "🧬",
|
||||
Financial: "💰",
|
||||
Identity: "🎭",
|
||||
Security: "🛡️",
|
||||
"Data Leaks": "💾",
|
||||
Weapons: "🔫",
|
||||
Forgery: "🖨️",
|
||||
Chemicals: "🧪",
|
||||
Confections: "🍬",
|
||||
Hardware: "🔧",
|
||||
"Digital goods": "📀",
|
||||
Services: "🛎️",
|
||||
"Alchemical Elixirs": "🧪",
|
||||
"Blood Pacts": "🩸",
|
||||
"Glamour Illusions": "🎭",
|
||||
"Ward Enchantments": "🛡️",
|
||||
"Forbidden Prophecies": "📜",
|
||||
"Eldritch Relics": "🗡️",
|
||||
"Soul Forging": "⚒️",
|
||||
"Necromantic Dust": "⚱️",
|
||||
"Fey Offerings": "🍄",
|
||||
"Ritual Focuses": "🔮",
|
||||
"Astral Projections": "✨",
|
||||
"Summoning Rites": "🕯️",
|
||||
};
|
||||
const m = new Map<string, number>();
|
||||
for (const p of SHOP_PRODUCTS) {
|
||||
|
||||
95
lib/siteNav.ts
Normal file
95
lib/siteNav.ts
Normal 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
364
onions.txt
Normal 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:8080–8122.
|
||||
# =============================================================================
|
||||
|
||||
# --- 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
|
||||
@@ -7,9 +7,10 @@
|
||||
"verify": "node scripts/verify.cjs",
|
||||
"dev": "next dev",
|
||||
"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",
|
||||
"health:stack": "bash scripts/health-check-stack.sh",
|
||||
"diagnose:onion": "bash scripts/diagnose-onion-stack.sh",
|
||||
"onions:status": "node scripts/onion-status.cjs",
|
||||
"onions:list": "bash scripts/list-onion-urls.sh",
|
||||
"install:systemd": "bash -c 'echo Run: sudo CYBERLUX_USER=$USER bash scripts/install-systemd.sh'",
|
||||
|
||||
54
proxy.ts
54
proxy.ts
@@ -6,65 +6,15 @@ import {
|
||||
type CyberluxEntry,
|
||||
} from "@/lib/cyberluxEntry";
|
||||
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>;` */
|
||||
function entryFromRequest(request: NextRequest): CyberluxEntry {
|
||||
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 {
|
||||
return CROSS_NAV_PREFIXES.some((p) => pathname.startsWith(p));
|
||||
return isOnionCrossNavPath(pathname);
|
||||
}
|
||||
|
||||
function withEntryCookie(res: NextResponse, request: NextRequest): NextResponse {
|
||||
|
||||
60
scripts/diagnose-onion-stack.sh
Normal file
60
scripts/diagnose-onion-stack.sh
Normal 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 ""
|
||||
@@ -66,6 +66,8 @@ read_host() {
|
||||
[[ "${missing}" -gt 0 ]] && echo "# Services pending: ${missing} (start Tor and wait ~30s)"
|
||||
} > "${OUT}"
|
||||
|
||||
chmod a+r "${OUT}" 2>/dev/null || true
|
||||
|
||||
echo "[*] Wrote ${OUT}"
|
||||
echo "[*] Services resolved: ${ready} / $((ready + missing))"
|
||||
[[ "${missing}" -gt 0 ]] && echo "[!] ${missing} hostname(s) not yet available — run again after Tor fully starts."
|
||||
|
||||
@@ -41,6 +41,7 @@ server {
|
||||
proxy_set_header X-Forwarded-Proto http;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header X-Cyberlux-Node ${h};
|
||||
}
|
||||
location /api/ {
|
||||
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-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto http;
|
||||
proxy_set_header X-Cyberlux-Node ${h};
|
||||
}
|
||||
location /forum {
|
||||
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-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto http;
|
||||
proxy_set_header X-Cyberlux-Node ${h};
|
||||
}
|
||||
location /exchange {
|
||||
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-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto http;
|
||||
proxy_set_header X-Cyberlux-Node ${h};
|
||||
}
|
||||
|
||||
location / {
|
||||
@@ -109,6 +113,7 @@ server {
|
||||
proxy_set_header X-Forwarded-Proto http;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header X-Cyberlux-Node "wiki";
|
||||
}
|
||||
location /api/ {
|
||||
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-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto http;
|
||||
proxy_set_header X-Cyberlux-Node "wiki";
|
||||
}
|
||||
location /forum {
|
||||
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-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto http;
|
||||
proxy_set_header X-Cyberlux-Node "wiki";
|
||||
}
|
||||
location /exchange {
|
||||
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-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto http;
|
||||
proxy_set_header X-Cyberlux-Node "wiki";
|
||||
}
|
||||
|
||||
location / {
|
||||
@@ -177,6 +185,7 @@ server {
|
||||
proxy_set_header X-Forwarded-Proto http;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header X-Cyberlux-Node "w";
|
||||
}
|
||||
location /api/ {
|
||||
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-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto http;
|
||||
proxy_set_header X-Cyberlux-Node "w";
|
||||
}
|
||||
location /forum {
|
||||
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-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto http;
|
||||
proxy_set_header X-Cyberlux-Node "w";
|
||||
}
|
||||
location /exchange {
|
||||
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-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto http;
|
||||
proxy_set_header X-Cyberlux-Node "w";
|
||||
}
|
||||
|
||||
location / {
|
||||
|
||||
0
scripts/health-check-stack.sh
Normal file → Executable file
0
scripts/health-check-stack.sh
Normal file → Executable file
@@ -79,7 +79,7 @@ EnvironmentFile=-${ENV_FILE}
|
||||
ExecStartPre=${NODE_BIN} ${CYBERLUX_REPO}/scripts/generate-onion-config.cjs
|
||||
|
||||
ExecStart=${NPM_BIN} run start:onion
|
||||
Restart=on-failure
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
# Avoid thrashing if Tor/nginx are still starting
|
||||
StartLimitIntervalSec=120
|
||||
|
||||
@@ -95,6 +95,21 @@ elif ! tor_is_active; then
|
||||
start_tor
|
||||
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
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -30,18 +30,30 @@ echo ""
|
||||
echo "CyberLux .onion URLs (http:// only — open in Tor Browser)"
|
||||
echo "────────────────────────────────────────────────────────────"
|
||||
|
||||
ready=0
|
||||
while IFS= read -r d || [[ -n "${d}" ]]; do
|
||||
[[ -z "${d}" ]] && continue
|
||||
f="/var/lib/tor/${d}/hostname"
|
||||
host="$(read_host "${f}")"
|
||||
if [[ -n "${host}" ]]; then
|
||||
printf '%-28s http://%s\n' "${d}" "${host}"
|
||||
((ready++)) || true
|
||||
else
|
||||
printf '%-28s (no hostname yet — %s)\n' "${d}" "${f}"
|
||||
fi
|
||||
done < "${TOR_DIRS}"
|
||||
|
||||
echo "────────────────────────────────────────────────────────────"
|
||||
echo ""
|
||||
echo "If lines show (no hostname yet): wait for Tor, or sudo ls -la /var/lib/tor/"
|
||||
if [[ "${ready}" -eq 0 ]] && [[ -f "${REPO}/onion-urls.txt" ]]; then
|
||||
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 ""
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
* node scripts/onion-status.cjs
|
||||
* 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
|
||||
*/
|
||||
|
||||
@@ -20,21 +21,46 @@ const REPO = path.join(__dirname, "..");
|
||||
const jsonPath = path.join(__dirname, "onion-nodes.json");
|
||||
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) {
|
||||
const f = path.join("/var/lib/tor", torDir, "hostname");
|
||||
try {
|
||||
return fs.readFileSync(f, "utf8").trim();
|
||||
} catch (e) {
|
||||
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") {
|
||||
const fromFile = readOnionFromExportFile(torDir);
|
||||
if (fromFile) return `${fromFile} (from onion-urls.txt)`;
|
||||
return "(no hostname yet — is Tor running?)";
|
||||
}
|
||||
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) {
|
||||
return new Promise((resolve) => {
|
||||
const req = http.request(
|
||||
@@ -77,14 +103,14 @@ async function main() {
|
||||
}
|
||||
|
||||
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));
|
||||
let bad = 0;
|
||||
for (const r of rows) {
|
||||
const url =
|
||||
r.onion.startsWith("(") || r.onion.includes("not readable")
|
||||
? r.onion
|
||||
: `http://${r.onion}`;
|
||||
: onionHttpUrl(r.onion);
|
||||
const local = `http://127.0.0.1:${r.port}/`;
|
||||
const status =
|
||||
r.httpCode === 0 ? "DOWN/timeout" : `HTTP ${r.httpCode}`;
|
||||
|
||||
19
scripts/start-onion-if-needed.sh
Executable file
19
scripts/start-onion-if-needed.sh
Executable 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
|
||||
@@ -20,8 +20,9 @@ try {
|
||||
run("TypeScript (tsc --noEmit)", "npx tsc --noEmit");
|
||||
run(
|
||||
"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");
|
||||
console.log("\n✓ verify: all checks passed.\n");
|
||||
} catch {
|
||||
|
||||
5
start.sh
5
start.sh
@@ -242,6 +242,11 @@ else
|
||||
sudo bash "${BACKUP_SCRIPT}" || echo "[!] Onion key backup refresh failed."
|
||||
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
|
||||
fi
|
||||
|
||||
|
||||
Reference in New Issue
Block a user