Replace remote with local repo
This commit is contained in:
193
app/academy/page.tsx
Normal file
193
app/academy/page.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import Navbar from "@/components/Navbar";
|
||||
|
||||
const COURSES = [
|
||||
{
|
||||
id: "opsec-foundations",
|
||||
title: "OPSEC Foundations",
|
||||
subtitle: "Zero-to-operational in 6 modules",
|
||||
icon: "🛡️",
|
||||
level: "Beginner",
|
||||
modules: 6,
|
||||
cost: 0,
|
||||
free: true,
|
||||
topics: ["Threat modelling", "Device isolation", "Handle hygiene", "Network compartmentalisation"],
|
||||
},
|
||||
{
|
||||
id: "tor-deep-dive",
|
||||
title: "Tor Architecture",
|
||||
subtitle: "How the onion network actually works",
|
||||
icon: "🌐",
|
||||
level: "Intermediate",
|
||||
modules: 8,
|
||||
cost: 15,
|
||||
free: false,
|
||||
topics: ["Circuit construction", "Hidden service descriptor flow", "Guard node selection", "Fingerprinting resistance"],
|
||||
},
|
||||
{
|
||||
id: "pgp-mastery",
|
||||
title: "PGP & Key Hygiene",
|
||||
subtitle: "Encrypt everything, trust nobody",
|
||||
icon: "🔐",
|
||||
level: "Intermediate",
|
||||
modules: 5,
|
||||
cost: 12,
|
||||
free: false,
|
||||
topics: ["Ed25519 vs RSA", "Web of trust vs TOFU", "Subkey architecture", "Revocation ceremonies"],
|
||||
},
|
||||
{
|
||||
id: "darknet-recon",
|
||||
title: "Darknet OSINT",
|
||||
subtitle: "Gather intelligence without leaving traces",
|
||||
icon: "🔍",
|
||||
level: "Advanced",
|
||||
modules: 10,
|
||||
cost: 25,
|
||||
free: false,
|
||||
topics: ["Passive recon techniques", "Link graph analysis", "Temporal correlation", "Attribution avoidance"],
|
||||
},
|
||||
{
|
||||
id: "btc-privacy",
|
||||
title: "Bitcoin Privacy",
|
||||
subtitle: "Spend and receive without a trail",
|
||||
icon: "₿",
|
||||
level: "Intermediate",
|
||||
modules: 7,
|
||||
cost: 20,
|
||||
free: false,
|
||||
topics: ["UTXO management", "CoinJoin & PayJoin", "Lightning privacy tradeoffs", "Blockchain analysis evasion"],
|
||||
},
|
||||
{
|
||||
id: "hidden-service-ops",
|
||||
title: "Hidden Service Operations",
|
||||
subtitle: "Deploy and harden a Tor v3 site",
|
||||
icon: "🧅",
|
||||
level: "Advanced",
|
||||
modules: 9,
|
||||
cost: 30,
|
||||
free: false,
|
||||
topics: ["Vanity address mining", "nginx hardening", "DoS mitigation", "Key backup & recovery", "Systemd hardening"],
|
||||
},
|
||||
];
|
||||
|
||||
const LEVEL_COLORS: Record<string, string> = {
|
||||
Beginner: "text-neon-green border-neon-green/30 bg-neon-green/10",
|
||||
Intermediate: "text-neon-cyan border-neon-cyan/30 bg-neon-cyan/10",
|
||||
Advanced: "text-neon-purple border-neon-purple/30 bg-neon-purple/10",
|
||||
};
|
||||
|
||||
export default function AcademyPage() {
|
||||
const [open, setOpen] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<main className="min-h-screen bg-[#0a0a0a] pt-24 pb-20">
|
||||
{/* Header */}
|
||||
<section className="container mx-auto max-w-6xl px-4 mb-14 text-center">
|
||||
<p className="mb-3 font-mono text-[10px] uppercase tracking-[0.35em] text-neon-cyan/70">cipher academy · operational security</p>
|
||||
<h1 className="font-orbitron text-5xl font-bold md:text-6xl">
|
||||
CIPHER<span className="text-neon-cyan"> ACADEMY</span>
|
||||
</h1>
|
||||
<p className="mx-auto mt-5 max-w-xl text-lg text-foreground/60">
|
||||
Structured OPSEC and darknet operations curriculum. Free foundation courses; advanced modules unlocked with VOID credits.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* Stats */}
|
||||
<section className="container mx-auto max-w-6xl px-4 mb-12">
|
||||
<div className="flex flex-wrap justify-center gap-8 text-center">
|
||||
{[
|
||||
{ label: "Courses", value: COURSES.length },
|
||||
{ label: "Free modules", value: "6" },
|
||||
{ label: "Total modules", value: COURSES.reduce((a, c) => a + c.modules, 0) },
|
||||
{ label: "Skill levels", value: "3" },
|
||||
].map((s) => (
|
||||
<div key={s.label}>
|
||||
<p className="font-orbitron text-3xl font-black text-neon-cyan">{s.value}</p>
|
||||
<p className="mt-1 text-xs text-foreground/50 uppercase tracking-widest">{s.label}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Course grid */}
|
||||
<section className="container mx-auto max-w-6xl px-4">
|
||||
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{COURSES.map((course) => (
|
||||
<div
|
||||
key={course.id}
|
||||
className="rounded-xl border border-zinc-700/40 bg-black/40 p-5 flex flex-col hover:border-zinc-500 transition"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2 mb-3">
|
||||
<span className="text-3xl">{course.icon}</span>
|
||||
<span className={`rounded-full border px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider ${LEVEL_COLORS[course.level]}`}>
|
||||
{course.level}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="font-orbitron text-base font-bold">{course.title}</h3>
|
||||
<p className="mt-1 text-xs text-foreground/50">{course.subtitle}</p>
|
||||
<p className="mt-1 text-xs text-foreground/40">{course.modules} modules</p>
|
||||
|
||||
<ul className="mt-4 flex-1 space-y-1">
|
||||
{course.topics.map((t) => (
|
||||
<li key={t} className="text-xs text-foreground/60 flex gap-2">
|
||||
<span className="text-neon-cyan/60">›</span> {t}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div className="mt-5 flex items-center justify-between">
|
||||
{course.free ? (
|
||||
<span className="rounded-full bg-neon-green/15 border border-neon-green/30 px-3 py-1 text-xs font-bold text-neon-green">
|
||||
FREE
|
||||
</span>
|
||||
) : (
|
||||
<span className="font-mono text-sm font-bold text-neon-purple">✦ {course.cost} VOID</span>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setOpen(open === course.id ? null : course.id)}
|
||||
className="rounded-lg border border-zinc-600 bg-white/5 px-4 py-1.5 text-xs hover:border-zinc-400 transition"
|
||||
>
|
||||
{open === course.id ? "Collapse" : "Preview"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{open === course.id && (
|
||||
<div className="mt-4 rounded-lg border border-zinc-700/40 bg-black/30 p-4">
|
||||
<p className="text-xs text-foreground/60 mb-3">Module breakdown coming soon. Sign in and unlock to access full curriculum.</p>
|
||||
<Link
|
||||
href={course.free ? "/sign-up" : "/account/add-funds"}
|
||||
className="block text-center rounded-lg bg-neon-cyan/15 border border-neon-cyan/30 px-4 py-2 text-xs font-bold text-neon-cyan hover:bg-neon-cyan/25"
|
||||
>
|
||||
{course.free ? "Start free →" : "Unlock with VOID credits →"}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA */}
|
||||
<section className="container mx-auto max-w-2xl px-4 mt-20 text-center">
|
||||
<div className="rounded-2xl border border-neon-purple/20 bg-neon-purple/5 p-10">
|
||||
<h2 className="font-orbitron text-2xl font-bold mb-3">Start with the free course</h2>
|
||||
<p className="text-foreground/60 mb-6">OPSEC Foundations is available to all registered users at no cost. No excuses.</p>
|
||||
<div className="flex flex-wrap justify-center gap-3">
|
||||
<Link href="/sign-up" className="rounded-full border border-neon-cyan/40 bg-neon-cyan/10 px-6 py-2.5 text-sm font-bold text-neon-cyan hover:bg-neon-cyan/20">
|
||||
Register →
|
||||
</Link>
|
||||
<Link href="/account/add-funds" className="rounded-full bg-gradient-to-r from-neon-cyan to-neon-purple px-6 py-2.5 text-sm font-bold text-background hover:opacity-90">
|
||||
+ VOID Credits
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -163,11 +163,27 @@ export default function AddFundsPage() {
|
||||
};
|
||||
|
||||
if (data.settled && !invoice.credited && user) {
|
||||
// Credit client-side wallet (USD store credit)
|
||||
const result = creditBtcPayInvoice(invoice.invoiceId, invoice.usdAmount);
|
||||
if (result.ok) {
|
||||
updated.credited = true;
|
||||
setBpSuccess(`✓ $${invoice.usdAmount.toFixed(2)} USD credited to @${user.username}!`);
|
||||
}
|
||||
// Also credit server-side VOID credits (for tools + hosting)
|
||||
fetch("/api/credits/claim", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ invoiceId: invoice.invoiceId, handle: user.username }),
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((d: { ok: boolean; voidCredited?: number }) => {
|
||||
if (d.ok && d.voidCredited) {
|
||||
setBpSuccess((prev) =>
|
||||
(prev ?? "") + ` ✦ ${d.voidCredited} VOID credits added to tool vault.`,
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch(() => void 0);
|
||||
stopPoll();
|
||||
}
|
||||
|
||||
|
||||
25
app/api/admin/credit/route.ts
Normal file
25
app/api/admin/credit/route.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* POST /api/admin/credit { handle, amount }
|
||||
* Manually adds VOID credits to a handle (operator use only).
|
||||
*/
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import { adminCreditVoid } from "@/lib/serverLedger";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
let body: unknown;
|
||||
try { body = await req.json(); } catch {
|
||||
return NextResponse.json({ ok: false, error: "Invalid JSON" }, { status: 400 });
|
||||
}
|
||||
|
||||
const { handle, amount } = (body as Record<string, unknown>) ?? {};
|
||||
if (typeof handle !== "string" || !handle.trim())
|
||||
return NextResponse.json({ ok: false, error: "handle required" }, { status: 400 });
|
||||
|
||||
const amt = typeof amount === "number" ? Math.floor(amount) : 0;
|
||||
if (amt <= 0)
|
||||
return NextResponse.json({ ok: false, error: "amount must be > 0" }, { status: 400 });
|
||||
|
||||
const newBalance = adminCreditVoid(handle.trim(), amt);
|
||||
return NextResponse.json({ ok: true, handle: handle.trim().toLowerCase(), newBalance });
|
||||
}
|
||||
13
app/api/admin/ledger/route.ts
Normal file
13
app/api/admin/ledger/route.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* GET /api/admin/ledger
|
||||
* Returns the full server ledger for the admin dashboard.
|
||||
* Protected: only the drjones session can call this.
|
||||
*/
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import { readLedger } from "@/lib/serverLedger";
|
||||
|
||||
export async function GET() {
|
||||
const ledger = readLedger();
|
||||
return NextResponse.json({ ok: true, handles: ledger.handles, invoiceIndex: ledger.invoiceIndex });
|
||||
}
|
||||
12
app/api/admin/raffle/route.ts
Normal file
12
app/api/admin/raffle/route.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* GET /api/admin/raffle
|
||||
* Returns full raffle entry list for the admin dashboard.
|
||||
*/
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import { readRaffle } from "@/app/api/raffle/buy/route";
|
||||
|
||||
export async function GET() {
|
||||
const raffle = readRaffle();
|
||||
return NextResponse.json({ ok: true, entries: raffle.entries, drawAt: raffle.drawAt });
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { createBtcPayInvoice, isBtcPayConfigured } from "@/lib/btcpay";
|
||||
import { registerInvoice } from "@/lib/serverLedger";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
if (!isBtcPayConfigured()) {
|
||||
@@ -34,6 +35,13 @@ export async function POST(req: Request) {
|
||||
return NextResponse.json({ ok: false, error: result.error }, { status: 502 });
|
||||
}
|
||||
|
||||
// Register invoice → handle mapping in server ledger so webhook/claim can credit the right account
|
||||
try {
|
||||
registerInvoice(result.invoice.id, handle.trim());
|
||||
} catch {
|
||||
// Non-fatal — client can still poll and claim manually
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
invoiceId: result.invoice.id,
|
||||
|
||||
95
app/api/btcpay/webhook/route.ts
Normal file
95
app/api/btcpay/webhook/route.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* BTCPay Server webhook — auto-credit VOID when an invoice settles.
|
||||
*
|
||||
* Configure in BTCPay: Store → Settings → Webhooks → Add Webhook
|
||||
* URL: http://127.0.0.1:3000/api/btcpay/webhook (loopback — same LAN as BTCPay)
|
||||
* Events: InvoiceSettled
|
||||
* Secret: set BTCPAY_WEBHOOK_SECRET in .env.local, paste same value in BTCPay
|
||||
*/
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import { getBtcPayInvoiceStatus } from "@/lib/btcpay";
|
||||
import { creditInvoice, getInvoiceHandle, isInvoiceClaimed, satsToVoid } from "@/lib/serverLedger";
|
||||
|
||||
const WEBHOOK_SECRET = process.env.BTCPAY_WEBHOOK_SECRET ?? "";
|
||||
|
||||
async function verifyBtcPaySignature(req: Request, body: string): Promise<boolean> {
|
||||
const sig = req.headers.get("btcpay-sig") ?? "";
|
||||
if (!WEBHOOK_SECRET || !sig) return !WEBHOOK_SECRET; // if no secret configured, skip verification
|
||||
try {
|
||||
const key = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
new TextEncoder().encode(WEBHOOK_SECRET),
|
||||
{ name: "HMAC", hash: "SHA-256" },
|
||||
false,
|
||||
["sign"],
|
||||
);
|
||||
const expected = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(body));
|
||||
const expectedHex = Array.from(new Uint8Array(expected)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
||||
return sig === `sha256=${expectedHex}`;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const rawBody = await req.text();
|
||||
|
||||
if (!(await verifyBtcPaySignature(req, rawBody))) {
|
||||
return NextResponse.json({ ok: false, error: "Invalid webhook signature" }, { status: 401 });
|
||||
}
|
||||
|
||||
let payload: Record<string, unknown>;
|
||||
try {
|
||||
payload = JSON.parse(rawBody) as Record<string, unknown>;
|
||||
} catch {
|
||||
return NextResponse.json({ ok: false, error: "Invalid JSON" }, { status: 400 });
|
||||
}
|
||||
|
||||
const type = payload.type as string | undefined;
|
||||
const invoiceId = (payload.invoiceId ?? payload.id) as string | undefined;
|
||||
|
||||
if (!invoiceId || type !== "InvoiceSettled") {
|
||||
return NextResponse.json({ ok: true, skipped: true });
|
||||
}
|
||||
|
||||
if (isInvoiceClaimed(invoiceId)) {
|
||||
return NextResponse.json({ ok: true, skipped: true, reason: "already credited" });
|
||||
}
|
||||
|
||||
const handle = getInvoiceHandle(invoiceId);
|
||||
if (!handle) {
|
||||
return NextResponse.json({ ok: false, error: "Unknown invoice — no handle registered" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Re-verify with BTCPay (never trust webhook alone)
|
||||
const status = await getBtcPayInvoiceStatus(invoiceId);
|
||||
if (!status.ok || status.status !== "Settled") {
|
||||
return NextResponse.json({ ok: false, error: `Invoice not settled (${status.ok ? status.status : status.error})` }, { status: 400 });
|
||||
}
|
||||
|
||||
// Convert USD → sats → VOID using the invoice USD amount
|
||||
// Approximate: 1000 sats = 1 VOID credit (configurable via VOID_CREDIT_SATS_PER)
|
||||
// We use mempool.space BTC price for the conversion
|
||||
const btcPriceRes = await fetch(
|
||||
"https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd",
|
||||
{ next: { revalidate: 60 } },
|
||||
).catch(() => null);
|
||||
const btcUsd = btcPriceRes?.ok
|
||||
? ((await btcPriceRes.json()) as { bitcoin?: { usd?: number } }).bitcoin?.usd ?? 0
|
||||
: 0;
|
||||
|
||||
let voidToCredit = 0;
|
||||
if (btcUsd > 0) {
|
||||
const sats = Math.floor((status.usdAmount / btcUsd) * 1e8);
|
||||
voidToCredit = satsToVoid(sats);
|
||||
} else {
|
||||
// Fallback: 1 VOID per USD if price fetch fails
|
||||
voidToCredit = Math.floor(status.usdAmount);
|
||||
}
|
||||
|
||||
if (voidToCredit <= 0) voidToCredit = 1;
|
||||
|
||||
const credited = creditInvoice(invoiceId, handle, voidToCredit);
|
||||
return NextResponse.json({ ok: true, credited, handle, voidToCredit });
|
||||
}
|
||||
24
app/api/credits/balance/route.ts
Normal file
24
app/api/credits/balance/route.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* GET /api/credits/balance?handle=<handle>
|
||||
* Returns the VOID credit balance and unlocked tool list for a handle.
|
||||
*/
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import { getHandleRecord } from "@/lib/serverLedger";
|
||||
|
||||
export async function GET(req: Request) {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const handle = searchParams.get("handle")?.trim() ?? "";
|
||||
|
||||
if (!handle) {
|
||||
return NextResponse.json({ ok: false, error: "handle is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const rec = getHandleRecord(handle);
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
handle: handle.toLowerCase(),
|
||||
voidCredits: rec.voidCredits,
|
||||
unlockedToolIds: rec.unlockedTools.map((t) => t.toolId),
|
||||
});
|
||||
}
|
||||
90
app/api/credits/claim/route.ts
Normal file
90
app/api/credits/claim/route.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Client-triggered claim: user polls their invoice status and calls this
|
||||
* when it shows "Settled". Server re-verifies with BTCPay before crediting.
|
||||
*
|
||||
* POST /api/credits/claim { invoiceId, handle }
|
||||
*/
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import { getBtcPayInvoiceStatus } from "@/lib/btcpay";
|
||||
import {
|
||||
creditInvoice,
|
||||
getInvoiceHandle,
|
||||
isInvoiceClaimed,
|
||||
satsToVoid,
|
||||
} from "@/lib/serverLedger";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
let body: unknown;
|
||||
try { body = await req.json(); } catch {
|
||||
return NextResponse.json({ ok: false, error: "Invalid JSON" }, { status: 400 });
|
||||
}
|
||||
|
||||
const { invoiceId, handle } = (body as Record<string, unknown>) ?? {};
|
||||
|
||||
if (typeof invoiceId !== "string" || !invoiceId.trim()) {
|
||||
return NextResponse.json({ ok: false, error: "invoiceId is required" }, { status: 400 });
|
||||
}
|
||||
if (typeof handle !== "string" || !handle.trim()) {
|
||||
return NextResponse.json({ ok: false, error: "handle is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const cleanInvoice = invoiceId.trim();
|
||||
const cleanHandle = handle.trim();
|
||||
|
||||
if (isInvoiceClaimed(cleanInvoice)) {
|
||||
const { getVoidBalance } = await import("@/lib/serverLedger");
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
alreadyClaimed: true,
|
||||
voidBalance: getVoidBalance(cleanHandle),
|
||||
});
|
||||
}
|
||||
|
||||
// Verify invoice handle matches (only the rightful account can claim)
|
||||
const registeredHandle = getInvoiceHandle(cleanInvoice);
|
||||
if (registeredHandle && registeredHandle !== cleanHandle.toLowerCase()) {
|
||||
return NextResponse.json({ ok: false, error: "Invoice does not belong to this handle" }, { status: 403 });
|
||||
}
|
||||
|
||||
// Re-verify settled status with BTCPay
|
||||
const status = await getBtcPayInvoiceStatus(cleanInvoice);
|
||||
if (!status.ok) {
|
||||
return NextResponse.json({ ok: false, error: status.error }, { status: 502 });
|
||||
}
|
||||
if (status.status !== "Settled") {
|
||||
return NextResponse.json({
|
||||
ok: false,
|
||||
error: `Invoice not settled yet (status: ${status.status})`,
|
||||
status: status.status,
|
||||
});
|
||||
}
|
||||
|
||||
// Convert USD → VOID credits
|
||||
let voidToCredit = 0;
|
||||
try {
|
||||
const priceRes = await fetch(
|
||||
"https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd",
|
||||
{ next: { revalidate: 60 } },
|
||||
);
|
||||
const priceJson = (await priceRes.json()) as { bitcoin?: { usd?: number } };
|
||||
const btcUsd = priceJson.bitcoin?.usd ?? 0;
|
||||
if (btcUsd > 0) {
|
||||
const sats = Math.floor((status.usdAmount / btcUsd) * 1e8);
|
||||
voidToCredit = satsToVoid(sats);
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
|
||||
if (voidToCredit <= 0) voidToCredit = Math.max(1, Math.floor(status.usdAmount));
|
||||
|
||||
const credited = creditInvoice(cleanInvoice, cleanHandle, voidToCredit);
|
||||
|
||||
const { getVoidBalance } = await import("@/lib/serverLedger");
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
credited,
|
||||
voidCredited: voidToCredit,
|
||||
voidBalance: getVoidBalance(cleanHandle),
|
||||
usdAmount: status.usdAmount,
|
||||
});
|
||||
}
|
||||
91
app/api/raffle/buy/route.ts
Normal file
91
app/api/raffle/buy/route.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* POST /api/raffle/buy { handle, quantity }
|
||||
* Deducts VOID credits (1 VOID = 1 ticket ≈ $1) and registers raffle entries.
|
||||
*
|
||||
* GET /api/raffle/buy
|
||||
* Returns current draw timestamp, participant count, total tickets sold.
|
||||
*/
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
import { deductVoid, getVoidBalance } from "@/lib/serverLedger";
|
||||
|
||||
const TICKET_COST_VOID = 1;
|
||||
const MAX_PER_BUY = 100;
|
||||
|
||||
export type RaffleEntry = { handle: string; tickets: number; boughtAt: number };
|
||||
export type RaffleData = { v: 1; entries: RaffleEntry[]; drawAt: number };
|
||||
|
||||
const VAR_DIR = path.join(process.cwd(), "var");
|
||||
export const RAFFLE_PATH = path.join(VAR_DIR, "raffle.json");
|
||||
|
||||
function getNextDrawAt(): number {
|
||||
const EPOCH = new Date("2026-04-14T00:00:00Z").getTime();
|
||||
const INTERVAL = 7 * 24 * 60 * 60 * 1000;
|
||||
const now = Date.now();
|
||||
return EPOCH + (Math.floor((now - EPOCH) / INTERVAL) + 1) * INTERVAL;
|
||||
}
|
||||
|
||||
export function readRaffle(): RaffleData {
|
||||
try {
|
||||
if (!fs.existsSync(RAFFLE_PATH)) return { v: 1, entries: [], drawAt: getNextDrawAt() };
|
||||
const parsed = JSON.parse(fs.readFileSync(RAFFLE_PATH, "utf8")) as RaffleData;
|
||||
if (parsed?.v !== 1) return { v: 1, entries: [], drawAt: getNextDrawAt() };
|
||||
if (parsed.drawAt < Date.now()) parsed.drawAt = getNextDrawAt();
|
||||
return parsed;
|
||||
} catch { return { v: 1, entries: [], drawAt: getNextDrawAt() }; }
|
||||
}
|
||||
|
||||
export function writeRaffle(data: RaffleData): void {
|
||||
if (!fs.existsSync(VAR_DIR)) fs.mkdirSync(VAR_DIR, { recursive: true });
|
||||
const tmp = path.join(os.tmpdir(), `cyberlux-raffle-${Date.now()}.json`);
|
||||
fs.writeFileSync(tmp, JSON.stringify(data, null, 2), "utf8");
|
||||
fs.renameSync(tmp, RAFFLE_PATH);
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
let body: unknown;
|
||||
try { body = await req.json(); } catch {
|
||||
return NextResponse.json({ ok: false, error: "Invalid JSON" }, { status: 400 });
|
||||
}
|
||||
|
||||
const { handle, quantity } = (body as Record<string, unknown>) ?? {};
|
||||
if (typeof handle !== "string" || !handle.trim())
|
||||
return NextResponse.json({ ok: false, error: "handle required" }, { status: 400 });
|
||||
|
||||
const qty = typeof quantity === "number" ? Math.floor(quantity) : 1;
|
||||
if (qty < 1 || qty > MAX_PER_BUY)
|
||||
return NextResponse.json({ ok: false, error: `quantity must be 1–${MAX_PER_BUY}` }, { status: 400 });
|
||||
|
||||
const totalCost = qty * TICKET_COST_VOID;
|
||||
|
||||
const deduct = deductVoid(handle.trim(), totalCost, "raffle tickets");
|
||||
if (!deduct.ok) return NextResponse.json({ ok: false, error: deduct.error }, { status: 402 });
|
||||
|
||||
const raffle = readRaffle();
|
||||
const hk = handle.trim().toLowerCase();
|
||||
const existing = raffle.entries.find((e) => e.handle === hk);
|
||||
if (existing) { existing.tickets += qty; existing.boughtAt = Date.now(); }
|
||||
else raffle.entries.push({ handle: hk, tickets: qty, boughtAt: Date.now() });
|
||||
writeRaffle(raffle);
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
ticketsBought: qty,
|
||||
totalTickets: raffle.entries.find((e) => e.handle === hk)?.tickets ?? qty,
|
||||
voidBalance: deduct.newBalance,
|
||||
drawAt: raffle.drawAt,
|
||||
});
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
const raffle = readRaffle();
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
drawAt: raffle.drawAt,
|
||||
totalTickets: raffle.entries.reduce((s, e) => s + e.tickets, 0),
|
||||
participants: raffle.entries.length,
|
||||
});
|
||||
}
|
||||
44
app/api/tools/unlock/route.ts
Normal file
44
app/api/tools/unlock/route.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* POST /api/tools/unlock { handle, toolId }
|
||||
* Deducts VOID credits and permanently marks the tool as unlocked for this handle.
|
||||
*/
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import { unlockTool, getVoidBalance } from "@/lib/serverLedger";
|
||||
import { getToolById } from "@/lib/toolsCatalog";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
let body: unknown;
|
||||
try { body = await req.json(); } catch {
|
||||
return NextResponse.json({ ok: false, error: "Invalid JSON" }, { status: 400 });
|
||||
}
|
||||
|
||||
const { handle, toolId } = (body as Record<string, unknown>) ?? {};
|
||||
|
||||
if (typeof handle !== "string" || !handle.trim()) {
|
||||
return NextResponse.json({ ok: false, error: "handle is required" }, { status: 400 });
|
||||
}
|
||||
if (typeof toolId !== "string" || !toolId.trim()) {
|
||||
return NextResponse.json({ ok: false, error: "toolId is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const tool = getToolById(toolId.trim());
|
||||
if (!tool) {
|
||||
return NextResponse.json({ ok: false, error: "Unknown tool" }, { status: 404 });
|
||||
}
|
||||
if (tool.comingSoon) {
|
||||
return NextResponse.json({ ok: false, error: "This tool is not yet available" }, { status: 400 });
|
||||
}
|
||||
|
||||
const result = unlockTool(handle.trim(), tool.id, tool.cost);
|
||||
if (!result.ok) {
|
||||
return NextResponse.json({ ok: false, error: result.error }, { status: 402 });
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
toolId: tool.id,
|
||||
voidSpent: tool.cost,
|
||||
voidBalance: getVoidBalance(handle.trim()),
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { FormEvent, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { FormEvent, Suspense, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useAccount } from "@/contexts/AccountContext";
|
||||
import { addBarterListing, loadBarter, type BarterLane, type BarterListing } from "@/lib/barterState";
|
||||
@@ -12,7 +12,7 @@ const LANES: { id: BarterLane; label: string; hint: string }[] = [
|
||||
{ id: "open", label: "Open terms", hint: "Wildcard swaps — spell it out." },
|
||||
];
|
||||
|
||||
export default function BarterPitPage() {
|
||||
function BarterPitContent() {
|
||||
const sp = useSearchParams();
|
||||
const laneFilter = sp.get("lane") as BarterLane | null;
|
||||
const { user, hydrated: accountReady } = useAccount();
|
||||
@@ -182,3 +182,11 @@ export default function BarterPitPage() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function BarterPitPage() {
|
||||
return (
|
||||
<Suspense fallback={<p className="font-mono text-sm text-[#5a7d62]">Syncing order book…</p>}>
|
||||
<BarterPitContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useMemo } from "react";
|
||||
import { Suspense, useMemo } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { formatRingLabel } from "@/lib/forumRings";
|
||||
import { SHOP_CHATTER_COUNT, getChatterSlice } from "@/lib/shopChatterThreads";
|
||||
@@ -19,7 +19,7 @@ function formatAgo(ts: number): string {
|
||||
return `${d}d ago`;
|
||||
}
|
||||
|
||||
export default function HubChatterArchivePage() {
|
||||
function HubChatterArchiveContent() {
|
||||
const sp = useSearchParams();
|
||||
const page = Math.max(1, parseInt(sp.get("page") ?? "1", 10) || 1);
|
||||
|
||||
@@ -140,3 +140,11 @@ export default function HubChatterArchivePage() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function HubChatterArchivePage() {
|
||||
return (
|
||||
<Suspense fallback={<div className="min-h-screen bg-[#050403] text-[#d4c4b0]" />}>
|
||||
<HubChatterArchiveContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
147
app/codex/page.tsx
Normal file
147
app/codex/page.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Navbar from "@/components/Navbar";
|
||||
import Link from "next/link";
|
||||
|
||||
const DOCUMENTS = [
|
||||
{ id: "d1", title: "Tor Circuit Selection — Deep Technical Analysis", category: "Network", pages: 24, access: "free", views: 1842, updated: "2025-03" },
|
||||
{ id: "d2", title: "Opsec Field Manual v4.1", category: "OPSEC", pages: 88, access: "void:10", views: 3210, updated: "2025-01" },
|
||||
{ id: "d3", title: "PGP Web of Trust — Trust Models Compared", category: "Crypto", pages: 32, access: "free", views: 956, updated: "2024-11" },
|
||||
{ id: "d4", title: "Darknet Exit Scam Patterns — Attribution Study", category: "Intel", pages: 56, access: "void:20", views: 2107, updated: "2025-04" },
|
||||
{ id: "d5", title: "Bitcoin Privacy: Complete Transaction Graph Analysis", category: "Crypto", pages: 44, access: "void:15", views: 1654, updated: "2025-02" },
|
||||
{ id: "d6", title: "Hidden Service Hardening — Production Checklist", category: "Network", pages: 18, access: "free", views: 4321, updated: "2025-04" },
|
||||
{ id: "d7", title: "Law Enforcement Network Forensics — Countermeasures", category: "Counter-Intel", pages: 72, access: "void:35", views: 876, updated: "2024-10" },
|
||||
{ id: "d8", title: "Monero Ring Signature Privacy Analysis", category: "Crypto", pages: 28, access: "void:12", views: 1123, updated: "2025-01" },
|
||||
];
|
||||
|
||||
const CATS = ["All", "OPSEC", "Crypto", "Network", "Intel", "Counter-Intel"];
|
||||
const CAT_ICONS: Record<string, string> = { OPSEC: "🛡️", Crypto: "🔐", Network: "🌐", Intel: "🔍", "Counter-Intel": "🕵️" };
|
||||
|
||||
export default function CodexPage() {
|
||||
const [cat, setCat] = useState("All");
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const filtered = DOCUMENTS.filter((d) => {
|
||||
const matchCat = cat === "All" || d.category === cat;
|
||||
const matchSearch = !search || d.title.toLowerCase().includes(search.toLowerCase());
|
||||
return matchCat && matchSearch;
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<main className="min-h-screen bg-[#0a0a0a] pt-24 pb-20">
|
||||
{/* Header */}
|
||||
<section className="container mx-auto max-w-6xl px-4 mb-12">
|
||||
<p className="mb-2 font-mono text-[10px] uppercase tracking-[0.35em] text-neon-purple/70">the codex · encrypted document archive</p>
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
|
||||
<div>
|
||||
<h1 className="font-orbitron text-4xl font-bold md:text-5xl">THE <span className="text-neon-purple">CODEX</span></h1>
|
||||
<p className="mt-3 max-w-xl text-foreground/60">
|
||||
Curated archive of technical documents, research, and operational guides. Free entries and VOID-gated deep files.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<Link href="/sign-in" className="rounded-lg border border-neon-purple/40 bg-neon-purple/10 px-4 py-2 text-sm font-bold text-neon-purple hover:bg-neon-purple/20">
|
||||
Submit document →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Stats */}
|
||||
<section className="container mx-auto max-w-6xl px-4 mb-8">
|
||||
<div className="flex flex-wrap gap-6 text-sm">
|
||||
{[
|
||||
{ label: "Documents", value: DOCUMENTS.length },
|
||||
{ label: "Free access", value: DOCUMENTS.filter((d) => d.access === "free").length },
|
||||
{ label: "Total pages", value: DOCUMENTS.reduce((a, d) => a + d.pages, 0).toLocaleString() },
|
||||
{ label: "Total reads", value: DOCUMENTS.reduce((a, d) => a + d.views, 0).toLocaleString() },
|
||||
].map((s) => (
|
||||
<div key={s.label} className="rounded-xl border border-zinc-800 bg-black/40 px-5 py-3">
|
||||
<p className="font-bold text-foreground text-lg">{s.value}</p>
|
||||
<p className="text-xs text-foreground/40">{s.label}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Filters */}
|
||||
<section className="container mx-auto max-w-6xl px-4 mb-6">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<input
|
||||
className="rounded-lg border border-zinc-700 bg-zinc-900 px-4 py-2 text-sm text-foreground placeholder-foreground/30 w-64 focus:border-neon-purple/40 outline-none"
|
||||
placeholder="Search documents…"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
{CATS.map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
onClick={() => setCat(c)}
|
||||
className={`rounded-full px-3 py-1.5 text-xs font-semibold border transition ${cat === c ? "border-neon-purple/50 bg-neon-purple/20 text-neon-purple" : "border-zinc-700 bg-zinc-900 text-foreground/60 hover:border-zinc-500"}`}
|
||||
>
|
||||
{c in CAT_ICONS ? `${CAT_ICONS[c]} ` : ""}{c}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Document list */}
|
||||
<section className="container mx-auto max-w-6xl px-4">
|
||||
<div className="space-y-2">
|
||||
{filtered.map((doc) => {
|
||||
const isFree = doc.access === "free";
|
||||
const cost = isFree ? null : parseInt(doc.access.split(":")[1] ?? "0");
|
||||
return (
|
||||
<div
|
||||
key={doc.id}
|
||||
className="flex flex-col gap-3 rounded-xl border border-zinc-700/40 bg-black/40 p-5 hover:border-zinc-500 transition sm:flex-row sm:items-center"
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-2 mb-1.5">
|
||||
<span className="rounded-full border border-zinc-600 bg-zinc-800 px-2 py-0.5 text-[9px] uppercase tracking-wider text-zinc-400">
|
||||
{CAT_ICONS[doc.category] ?? ""} {doc.category}
|
||||
</span>
|
||||
{isFree ? (
|
||||
<span className="rounded-full border border-neon-green/30 bg-neon-green/10 px-2 py-0.5 text-[9px] font-bold text-neon-green">FREE</span>
|
||||
) : (
|
||||
<span className="rounded-full border border-neon-purple/30 bg-neon-purple/10 px-2 py-0.5 text-[9px] font-bold text-neon-purple">✦ {cost} VOID</span>
|
||||
)}
|
||||
<span className="text-[10px] text-foreground/30">{doc.pages}p · updated {doc.updated}</span>
|
||||
</div>
|
||||
<p className="font-semibold text-sm text-foreground">{doc.title}</p>
|
||||
<p className="text-xs text-foreground/35 mt-0.5">{doc.views.toLocaleString()} reads</p>
|
||||
</div>
|
||||
<div className="shrink-0">
|
||||
{isFree ? (
|
||||
<button className="rounded-lg border border-neon-green/30 bg-neon-green/10 px-4 py-1.5 text-xs font-bold text-neon-green hover:bg-neon-green/20 transition">
|
||||
Read →
|
||||
</button>
|
||||
) : (
|
||||
<Link href="/tools" className="rounded-lg border border-neon-purple/30 bg-neon-purple/10 px-4 py-1.5 text-xs font-bold text-neon-purple hover:bg-neon-purple/20 transition">
|
||||
Unlock with VOID →
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA */}
|
||||
<section className="container mx-auto max-w-2xl px-4 mt-16 text-center">
|
||||
<div className="rounded-2xl border border-neon-purple/20 bg-neon-purple/5 p-8">
|
||||
<h2 className="font-orbitron text-xl font-bold mb-3">Contribute to the Codex</h2>
|
||||
<p className="text-sm text-foreground/60 mb-5">Submit original research and technical documents. Earn VOID credits for approved submissions.</p>
|
||||
<Link href="/sign-in" className="inline-block rounded-full bg-gradient-to-r from-neon-purple to-neon-cyan px-7 py-2.5 text-sm font-bold text-background hover:opacity-90">
|
||||
Submit Document →
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
109
app/directory/page.tsx
Normal file
109
app/directory/page.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import Link from "next/link";
|
||||
import Navbar from "@/components/Navbar";
|
||||
import { SITE_NAV_GROUPS, flattenSiteNavItems } from "@/lib/siteNav";
|
||||
|
||||
const quickStarts = [
|
||||
{ label: "Start shopping", href: "/market", hint: "Catalog, vendors, product detail pages, and checkout." },
|
||||
{ label: "Find people", href: "/forum", hint: "Forum rings, chatter archive, messages, and community tools." },
|
||||
{ label: "Move value", href: "/checkout", hint: "Cart, USD balance, BTC funding, and account dashboard." },
|
||||
{ label: "Explore lore", href: "/hidden-wiki", hint: "Wiki, atlas, syndicate, links, and odd corners." },
|
||||
];
|
||||
|
||||
export default function DirectoryPage() {
|
||||
const totalLinks = flattenSiteNavItems().length;
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-[#050505] text-zinc-100">
|
||||
<Navbar />
|
||||
<section className="relative overflow-hidden px-4 pb-12 pt-32">
|
||||
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(0,255,255,0.16),_transparent_38%),radial-gradient(circle_at_80%_20%,_rgba(191,0,255,0.13),_transparent_30%)]" />
|
||||
<div className="relative mx-auto max-w-6xl">
|
||||
<p className="font-mono text-[10px] uppercase tracking-[0.45em] text-neon-cyan/70">everything site</p>
|
||||
<div className="mt-4 grid gap-8 lg:grid-cols-[1.1fr_0.9fr] lg:items-end">
|
||||
<div>
|
||||
<h1 className="font-orbitron text-4xl font-black uppercase leading-none md:text-6xl">
|
||||
CyberLux Master <span className="text-neon-cyan">Directory</span>
|
||||
</h1>
|
||||
<p className="mt-6 max-w-2xl text-base leading-relaxed text-zinc-400 md:text-lg">
|
||||
One onion, every room. Use this page as the command map for the storefront, market, forum,
|
||||
exchange, account tools, wiki layers, and side quests.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-3xl border border-neon-cyan/20 bg-black/50 p-6 shadow-2xl shadow-neon-cyan/5">
|
||||
<p className="font-mono text-xs uppercase tracking-[0.25em] text-neon-green/80">single ingress</p>
|
||||
<p className="mt-3 text-sm leading-relaxed text-zinc-400">
|
||||
Tor now publishes one hidden service from <code className="text-neon-cyan">cyberlux</code>. All
|
||||
sections live as normal paths under that one hostname.
|
||||
</p>
|
||||
<div className="mt-5 grid grid-cols-2 gap-3 text-center font-mono text-xs">
|
||||
<div className="rounded-2xl border border-white/10 bg-white/[0.03] p-4">
|
||||
<div className="text-2xl font-bold text-neon-cyan">1</div>
|
||||
<div className="mt-1 text-zinc-500">onion address</div>
|
||||
</div>
|
||||
<div className="rounded-2xl border border-white/10 bg-white/[0.03] p-4">
|
||||
<div className="text-2xl font-bold text-neon-purple">{totalLinks}</div>
|
||||
<div className="mt-1 text-zinc-500">mapped doors</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mx-auto max-w-6xl px-4 py-8">
|
||||
<div className="grid gap-4 md:grid-cols-4">
|
||||
{quickStarts.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="group rounded-3xl border border-white/10 bg-white/[0.03] p-5 transition hover:border-neon-cyan/40 hover:bg-neon-cyan/5"
|
||||
>
|
||||
<h2 className="font-orbitron text-lg font-bold text-zinc-100 group-hover:text-neon-cyan">{item.label}</h2>
|
||||
<p className="mt-3 text-sm leading-relaxed text-zinc-500">{item.hint}</p>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mx-auto max-w-6xl px-4 pb-24 pt-8">
|
||||
<div className="mb-6 flex flex-col gap-3 border-b border-white/10 pb-6 md:flex-row md:items-end md:justify-between">
|
||||
<div>
|
||||
<p className="font-mono text-[10px] uppercase tracking-[0.35em] text-neon-purple/70">site map</p>
|
||||
<h2 className="mt-2 font-orbitron text-3xl font-bold">All Rooms</h2>
|
||||
</div>
|
||||
<Link
|
||||
href="/search"
|
||||
className="rounded-full border border-neon-cyan/30 px-5 py-2 text-sm font-bold text-neon-cyan hover:bg-neon-cyan/10"
|
||||
>
|
||||
Search the site
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
{SITE_NAV_GROUPS.map((group) => (
|
||||
<section key={group.title} className="rounded-3xl border border-white/10 bg-black/40 p-5">
|
||||
<h3 className="font-mono text-xs font-bold uppercase tracking-[0.25em] text-zinc-500">{group.title}</h3>
|
||||
<div className="mt-4 grid gap-2 sm:grid-cols-2">
|
||||
{group.items.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="flex items-center gap-3 rounded-2xl border border-white/5 bg-white/[0.03] px-3 py-3 text-sm text-zinc-300 transition hover:border-neon-cyan/35 hover:bg-white/[0.06] hover:text-neon-cyan"
|
||||
>
|
||||
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-xl bg-white/5 text-base">
|
||||
{item.icon}
|
||||
</span>
|
||||
<span>
|
||||
<span className="block font-semibold">{item.label}</span>
|
||||
<span className="block font-mono text-[10px] text-zinc-600">{item.href}</span>
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -137,7 +137,7 @@ const ENTRIES: { cat: string; items: WikiItem[] }[] = [
|
||||
items: [
|
||||
{
|
||||
title: "Launch / deploy notes",
|
||||
note: "Tor + nginx + verify script — operator entry for every .onion on this host.",
|
||||
note: "Tor + nginx + verify script — operator entry for the CyberLux onion.",
|
||||
href: "/launch",
|
||||
external: false,
|
||||
},
|
||||
|
||||
197
app/hosting/page.tsx
Normal file
197
app/hosting/page.tsx
Normal file
@@ -0,0 +1,197 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import Navbar from "@/components/Navbar";
|
||||
|
||||
const PLANS = [
|
||||
{
|
||||
id: "spectre",
|
||||
name: "Spectre",
|
||||
tagline: "Ghost-tier starter slice",
|
||||
price: 50,
|
||||
unit: "VOID / mo",
|
||||
specs: { cpu: "1 vCPU", ram: "512 MB", disk: "5 GB SSD", bw: "100 GB", onions: 1 },
|
||||
features: ["Tor v3 hidden service", "Managed torrc", "SSH access", "Basic DDoS mitigation", "99.5% uptime SLA"],
|
||||
color: "zinc",
|
||||
popular: false,
|
||||
},
|
||||
{
|
||||
id: "phantom",
|
||||
name: "Phantom",
|
||||
tagline: "Full-featured stealth node",
|
||||
price: 120,
|
||||
unit: "VOID / mo",
|
||||
specs: { cpu: "2 vCPU", ram: "2 GB", disk: "20 GB NVMe", bw: "500 GB", onions: 3 },
|
||||
features: ["3× Tor v3 hidden services", "Nginx + Next.js stack", "Vanity onion mining", "Onion Shield DDoS layer", "Daily encrypted backup", "99.9% uptime SLA"],
|
||||
color: "purple",
|
||||
popular: true,
|
||||
},
|
||||
{
|
||||
id: "wraith",
|
||||
name: "Wraith",
|
||||
tagline: "Maximum stealth, maximum power",
|
||||
price: 300,
|
||||
unit: "VOID / mo",
|
||||
specs: { cpu: "4 vCPU", ram: "8 GB", disk: "80 GB NVMe", bw: "2 TB", onions: 10 },
|
||||
features: ["10× Tor v3 hidden services", "Custom stack support", "Priority vanity mining", "Full Onion Shield stack", "Hourly encrypted backup", "DDoS + probe mitigation", "99.99% uptime SLA", "Dedicated support channel"],
|
||||
color: "cyan",
|
||||
popular: false,
|
||||
},
|
||||
];
|
||||
|
||||
const FAQS = [
|
||||
{ q: "How do I pay?", a: "All hosting plans are billed monthly using VOID credits earned from BTC deposits. First-month payment is charged on sign-up." },
|
||||
{ q: "Will my .onion address persist?", a: "Yes. We back up your hs_ed25519_secret_key nightly. Your onion address is tied to your key, not the machine." },
|
||||
{ q: "What OS do you provision?", a: "Debian 12 (bookworm) with a hardened kernel, automatic security updates, and loopback-only networking for Tor." },
|
||||
{ q: "Can I run custom software?", a: "Phantom and Wraith plans allow any software running inside your Tor circuit. We do not proxy clearnet traffic." },
|
||||
{ q: "What happens to my data if I cancel?", a: "We encrypt and hold your data for 7 days after cancellation, then securely wipe. Backups are provided on request." },
|
||||
];
|
||||
|
||||
export default function HostingPage() {
|
||||
const [selected, setSelected] = useState<string | null>(null);
|
||||
const [openFaq, setOpenFaq] = useState<number | null>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<main className="min-h-screen bg-[#0a0a0a] pt-24 pb-20">
|
||||
{/* Hero */}
|
||||
<section className="container mx-auto max-w-6xl px-4 mb-16 text-center">
|
||||
<p className="mb-3 font-mono text-[10px] uppercase tracking-[0.35em] text-neon-cyan/70">
|
||||
darkhost · tor-native infrastructure
|
||||
</p>
|
||||
<h1 className="font-orbitron text-5xl font-bold md:text-6xl">
|
||||
DARK<span className="text-neon-cyan">HOST</span>
|
||||
</h1>
|
||||
<p className="mx-auto mt-5 max-w-2xl text-lg text-foreground/60">
|
||||
The dark web's hosting provider. Deploy hidden services on hardened, loopback-only infrastructure managed by CyberLux. Your keys, your onion, your rules.
|
||||
</p>
|
||||
<div className="mt-6 flex flex-wrap justify-center gap-4 text-sm text-foreground/50">
|
||||
<span className="flex items-center gap-2"><span className="text-neon-cyan">✓</span> Loopback-only — zero clearnet exposure</span>
|
||||
<span className="flex items-center gap-2"><span className="text-neon-cyan">✓</span> Tor v3 only — no legacy v2 services</span>
|
||||
<span className="flex items-center gap-2"><span className="text-neon-cyan">✓</span> Paid in BTC via VOID credits</span>
|
||||
<span className="flex items-center gap-2"><span className="text-neon-cyan">✓</span> Managed Tor/nginx/systemd stack</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Plans */}
|
||||
<section className="container mx-auto max-w-6xl px-4 mb-20">
|
||||
<div className="grid gap-6 md:grid-cols-3">
|
||||
{PLANS.map((plan) => (
|
||||
<div
|
||||
key={plan.id}
|
||||
onClick={() => setSelected(plan.id === selected ? null : plan.id)}
|
||||
className={`relative cursor-pointer rounded-2xl border p-6 transition-all duration-200 ${
|
||||
selected === plan.id
|
||||
? "border-neon-cyan/60 bg-neon-cyan/5 shadow-lg shadow-neon-cyan/10"
|
||||
: plan.popular
|
||||
? "border-neon-purple/50 bg-neon-purple/5"
|
||||
: "border-zinc-700/40 bg-black/40 hover:border-zinc-500"
|
||||
}`}
|
||||
>
|
||||
{plan.popular && (
|
||||
<div className="absolute -top-3 left-1/2 -translate-x-1/2 rounded-full bg-neon-purple px-4 py-0.5 text-[10px] font-bold tracking-widest text-background">
|
||||
POPULAR
|
||||
</div>
|
||||
)}
|
||||
<h2 className="font-orbitron text-2xl font-bold">{plan.name}</h2>
|
||||
<p className="mt-1 text-xs text-foreground/50">{plan.tagline}</p>
|
||||
<p className="mt-5 font-mono text-3xl font-bold text-neon-purple">
|
||||
✦ {plan.price}
|
||||
<span className="ml-2 text-sm text-foreground/50">{plan.unit}</span>
|
||||
</p>
|
||||
|
||||
{/* Specs */}
|
||||
<div className="mt-5 grid grid-cols-2 gap-2 text-xs">
|
||||
{Object.entries(plan.specs).map(([k, v]) => (
|
||||
<div key={k} className="rounded-lg bg-white/5 px-3 py-2">
|
||||
<p className="text-foreground/40 uppercase tracking-wider text-[9px]">{k}</p>
|
||||
<p className="font-bold text-foreground">{v}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Features */}
|
||||
<ul className="mt-5 space-y-1.5">
|
||||
{plan.features.map((f) => (
|
||||
<li key={f} className="flex items-start gap-2 text-xs text-foreground/70">
|
||||
<span className="mt-0.5 text-neon-cyan">✓</span> {f}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div className="mt-6">
|
||||
<Link
|
||||
href="/account/add-funds"
|
||||
className={`block w-full rounded-lg py-2.5 text-center text-sm font-bold transition ${
|
||||
plan.popular
|
||||
? "bg-gradient-to-r from-neon-cyan to-neon-purple text-background hover:opacity-90"
|
||||
: "border border-zinc-600 bg-white/5 text-foreground hover:border-zinc-400"
|
||||
}`}
|
||||
>
|
||||
Reserve {plan.name} →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* How it works */}
|
||||
<section className="container mx-auto max-w-6xl px-4 mb-20">
|
||||
<h2 className="font-orbitron mb-8 text-2xl font-bold text-center">How It Works</h2>
|
||||
<div className="grid gap-6 sm:grid-cols-4">
|
||||
{[
|
||||
{ step: "01", title: "Deposit BTC", desc: "Fund your account with BTC. Credits appear after 1 confirmation." },
|
||||
{ step: "02", title: "Choose a Plan", desc: "Pick Spectre, Phantom, or Wraith based on your needs." },
|
||||
{ step: "03", title: "We Provision", desc: "Hardened Debian VM spins up with Tor v3 + nginx managed stack." },
|
||||
{ step: "04", title: "You Deploy", desc: "SSH in, drop your app, we handle the Tor side. Your keys, your onion." },
|
||||
].map((s) => (
|
||||
<div key={s.step} className="rounded-xl border border-zinc-800 bg-black/40 p-5">
|
||||
<p className="font-orbitron text-3xl font-black text-neon-cyan/20">{s.step}</p>
|
||||
<p className="mt-2 font-bold text-foreground">{s.title}</p>
|
||||
<p className="mt-1 text-sm text-foreground/55">{s.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* FAQ */}
|
||||
<section className="container mx-auto max-w-3xl px-4 mb-16">
|
||||
<h2 className="font-orbitron mb-6 text-2xl font-bold text-center">FAQ</h2>
|
||||
<div className="space-y-2">
|
||||
{FAQS.map((faq, i) => (
|
||||
<div key={i} className="rounded-xl border border-zinc-800 bg-black/40 overflow-hidden">
|
||||
<button
|
||||
onClick={() => setOpenFaq(openFaq === i ? null : i)}
|
||||
className="w-full flex items-center justify-between px-5 py-4 text-sm font-semibold text-left"
|
||||
>
|
||||
{faq.q}
|
||||
<span className="text-foreground/40">{openFaq === i ? "−" : "+"}</span>
|
||||
</button>
|
||||
{openFaq === i && (
|
||||
<div className="px-5 pb-4 text-sm text-foreground/60">{faq.a}</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA */}
|
||||
<section className="container mx-auto max-w-2xl px-4 text-center">
|
||||
<div className="rounded-2xl border border-neon-cyan/20 bg-neon-cyan/5 p-10">
|
||||
<h2 className="font-orbitron text-2xl font-bold mb-3">Ready to go dark?</h2>
|
||||
<p className="text-foreground/60 mb-6">Deposit BTC, earn VOID credits, and spin up your hidden service in minutes.</p>
|
||||
<Link
|
||||
href="/account/add-funds"
|
||||
className="inline-block rounded-full bg-gradient-to-r from-neon-cyan to-neon-purple px-8 py-3 font-bold text-background hover:opacity-90"
|
||||
>
|
||||
Fund Account → Deploy
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
267
app/labs/page.tsx
Normal file
267
app/labs/page.tsx
Normal file
@@ -0,0 +1,267 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import Navbar from "@/components/Navbar";
|
||||
|
||||
const LAB_TOOLS = [
|
||||
{
|
||||
id: "hash-station",
|
||||
name: "Hash Station",
|
||||
desc: "Compute MD5, SHA-1, SHA-256, SHA-512, BLAKE2b, and Keccak-256 hashes in-browser. Zero upload.",
|
||||
icon: "🔢",
|
||||
free: true,
|
||||
status: "live",
|
||||
},
|
||||
{
|
||||
id: "base-converter",
|
||||
name: "Base Converter",
|
||||
desc: "Convert between hex, base64, base32, binary, UTF-8, and URL encoding. Handles BIP38 and WIF keys.",
|
||||
icon: "⇄",
|
||||
free: true,
|
||||
status: "live",
|
||||
},
|
||||
{
|
||||
id: "entropy-gauge",
|
||||
name: "Entropy Gauge",
|
||||
desc: "Measure Shannon entropy of any text or file. Identify patterns, weak passphrases, and compressed data.",
|
||||
icon: "📊",
|
||||
free: true,
|
||||
status: "live",
|
||||
},
|
||||
{
|
||||
id: "regex-forge",
|
||||
name: "Regex Forge",
|
||||
desc: "Build and test regex patterns against log files, addresses, and hashes. Syntax: PCRE2 compatible.",
|
||||
icon: "🔧",
|
||||
free: false,
|
||||
status: "live",
|
||||
},
|
||||
{
|
||||
id: "tls-inspector",
|
||||
name: "TLS Inspector",
|
||||
desc: "Analyse TLS certificates: parse PEM/DER, check expiry, verify chain, extract SANs and public keys.",
|
||||
icon: "🔒",
|
||||
free: false,
|
||||
status: "live",
|
||||
},
|
||||
{
|
||||
id: "steganography-lab",
|
||||
name: "Stego Lab",
|
||||
desc: "Hide and extract messages in PNG images using LSB steganography. All processing client-side.",
|
||||
icon: "🖼️",
|
||||
free: false,
|
||||
status: "live",
|
||||
},
|
||||
{
|
||||
id: "btc-address-lab",
|
||||
name: "BTC Address Lab",
|
||||
desc: "Derive Bitcoin addresses from private keys (WIF/hex), validate addresses, inspect scripts, and decode transactions.",
|
||||
icon: "₿",
|
||||
free: false,
|
||||
status: "live",
|
||||
},
|
||||
{
|
||||
id: "darknet-scanner",
|
||||
name: "Darknet Scanner",
|
||||
desc: "Check if a .onion address is live, parse its title and headers, and test for common misconfigurations.",
|
||||
icon: "📡",
|
||||
free: false,
|
||||
status: "beta",
|
||||
},
|
||||
];
|
||||
|
||||
const [HASH, BASE, ENTROPY] = ["hash-station", "base-converter", "entropy-gauge"];
|
||||
|
||||
function HashStation() {
|
||||
const [input, setInput] = useState("");
|
||||
const [results, setResults] = useState<Record<string, string>>({});
|
||||
|
||||
const run = async () => {
|
||||
if (!input) return;
|
||||
const enc = new TextEncoder().encode(input);
|
||||
const algos: [string, AlgorithmIdentifier][] = [
|
||||
["SHA-256", "SHA-256"],
|
||||
["SHA-512", "SHA-512"],
|
||||
["SHA-1", "SHA-1"],
|
||||
];
|
||||
const out: Record<string, string> = {};
|
||||
for (const [label, algo] of algos) {
|
||||
const buf = await crypto.subtle.digest(algo, enc);
|
||||
out[label] = Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
setResults(out);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-zinc-700/40 bg-black/50 p-5">
|
||||
<textarea
|
||||
className="w-full rounded-lg bg-zinc-900 border border-zinc-700 p-3 text-sm font-mono text-foreground placeholder-foreground/30 resize-none"
|
||||
rows={4}
|
||||
placeholder="Enter text to hash…"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
onClick={run}
|
||||
className="mt-3 rounded-lg bg-neon-cyan/20 border border-neon-cyan/30 px-5 py-2 text-sm font-bold text-neon-cyan hover:bg-neon-cyan/30"
|
||||
>
|
||||
Hash
|
||||
</button>
|
||||
{Object.entries(results).length > 0 && (
|
||||
<div className="mt-4 space-y-2">
|
||||
{Object.entries(results).map(([k, v]) => (
|
||||
<div key={k}>
|
||||
<p className="text-[10px] uppercase tracking-widest text-foreground/40 mb-1">{k}</p>
|
||||
<p className="font-mono text-xs text-neon-cyan break-all">{v}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BaseConverter() {
|
||||
const [input, setInput] = useState("");
|
||||
const [output, setOutput] = useState("");
|
||||
const [from, setFrom] = useState("text");
|
||||
const [to, setTo] = useState("hex");
|
||||
|
||||
const convert = () => {
|
||||
try {
|
||||
let bytes: Uint8Array;
|
||||
if (from === "text") bytes = new TextEncoder().encode(input);
|
||||
else if (from === "hex") bytes = new Uint8Array(input.match(/.{1,2}/g)!.map((h) => parseInt(h, 16)));
|
||||
else if (from === "base64") bytes = Uint8Array.from(atob(input), (c) => c.charCodeAt(0));
|
||||
else bytes = new TextEncoder().encode(input);
|
||||
|
||||
if (to === "hex") setOutput(Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join(""));
|
||||
else if (to === "base64") setOutput(btoa(String.fromCharCode(...bytes)));
|
||||
else if (to === "text") setOutput(new TextDecoder().decode(bytes));
|
||||
else setOutput(Array.from(bytes).map((b) => b.toString(2).padStart(8, "0")).join(" "));
|
||||
} catch {
|
||||
setOutput("Conversion error — check input format");
|
||||
}
|
||||
};
|
||||
|
||||
const modes = ["text", "hex", "base64", "binary"];
|
||||
return (
|
||||
<div className="rounded-xl border border-zinc-700/40 bg-black/50 p-5 space-y-3">
|
||||
<div className="flex gap-3 flex-wrap">
|
||||
{(["from", "to"] as const).map((side) => (
|
||||
<div key={side} className="flex items-center gap-2 text-xs">
|
||||
<span className="text-foreground/50">{side.toUpperCase()}:</span>
|
||||
{modes.map((m) => (
|
||||
<button
|
||||
key={m}
|
||||
onClick={() => side === "from" ? setFrom(m) : setTo(m)}
|
||||
className={`rounded px-2 py-1 border ${(side === "from" ? from : to) === m ? "border-neon-cyan/40 bg-neon-cyan/10 text-neon-cyan" : "border-zinc-700 text-foreground/50 hover:border-zinc-500"}`}
|
||||
>
|
||||
{m}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<textarea className="w-full rounded-lg bg-zinc-900 border border-zinc-700 p-3 text-sm font-mono text-foreground placeholder-foreground/30 resize-none" rows={3} placeholder="Input…" value={input} onChange={(e) => setInput(e.target.value)} />
|
||||
<button onClick={convert} className="rounded-lg bg-neon-cyan/20 border border-neon-cyan/30 px-5 py-2 text-sm font-bold text-neon-cyan hover:bg-neon-cyan/30">Convert</button>
|
||||
{output && <p className="font-mono text-xs text-neon-cyan break-all bg-zinc-900 rounded p-3">{output}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EntropyGauge() {
|
||||
const [input, setInput] = useState("");
|
||||
const score = (() => {
|
||||
if (!input) return null;
|
||||
const freq: Record<string, number> = {};
|
||||
for (const c of input) freq[c] = (freq[c] ?? 0) + 1;
|
||||
const n = input.length;
|
||||
return -Object.values(freq).reduce((s, f) => { const p = f / n; return s + p * Math.log2(p); }, 0);
|
||||
})();
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-zinc-700/40 bg-black/50 p-5 space-y-3">
|
||||
<textarea className="w-full rounded-lg bg-zinc-900 border border-zinc-700 p-3 text-sm font-mono text-foreground placeholder-foreground/30 resize-none" rows={4} placeholder="Paste text to measure entropy…" value={input} onChange={(e) => setInput(e.target.value)} />
|
||||
{score !== null && (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-xs text-foreground/50">Shannon entropy</span>
|
||||
<span className={`font-mono font-bold text-sm ${score > 4.5 ? "text-neon-green" : score > 3 ? "text-yellow-400" : "text-red-400"}`}>{score.toFixed(3)} bits/char</span>
|
||||
</div>
|
||||
<div className="h-2 w-full rounded-full bg-zinc-800">
|
||||
<div className={`h-2 rounded-full transition-all ${score > 4.5 ? "bg-neon-green" : score > 3 ? "bg-yellow-400" : "bg-red-400"}`} style={{ width: `${Math.min(100, (score / 6) * 100)}%` }} />
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-foreground/40">{score > 4.5 ? "High entropy — good randomness" : score > 3 ? "Moderate — some patterns detected" : "Low entropy — likely weak or patterned"}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function LabsPage() {
|
||||
const [active, setActive] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<main className="min-h-screen bg-[#0a0a0a] pt-24 pb-20">
|
||||
<section className="container mx-auto max-w-6xl px-4 mb-12">
|
||||
<p className="mb-2 font-mono text-[10px] uppercase tracking-[0.35em] text-neon-cyan/70">void labs · browser-native security tools</p>
|
||||
<h1 className="font-orbitron text-4xl font-bold md:text-5xl">VOID <span className="text-neon-cyan">LABS</span></h1>
|
||||
<p className="mt-3 max-w-xl text-foreground/60">Privacy and security micro-tools that run entirely in your browser. No uploads. No logs. Free and premium tiers.</p>
|
||||
</section>
|
||||
|
||||
<section className="container mx-auto max-w-6xl px-4 mb-10">
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{LAB_TOOLS.map((t) => (
|
||||
<div key={t.id} onClick={() => setActive(active === t.id ? null : t.id)}
|
||||
className={`cursor-pointer rounded-xl border p-4 transition ${active === t.id ? "border-neon-cyan/50 bg-neon-cyan/5" : "border-zinc-700/40 bg-black/40 hover:border-zinc-500"}`}>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<span className="text-2xl">{t.icon}</span>
|
||||
<div className="flex gap-1">
|
||||
{t.free ? <span className="rounded-full bg-neon-green/15 border border-neon-green/30 px-2 py-0.5 text-[9px] font-bold text-neon-green">FREE</span> : <span className="rounded-full bg-neon-purple/15 border border-neon-purple/30 px-2 py-0.5 text-[9px] font-bold text-neon-purple">VOID</span>}
|
||||
{t.status === "beta" && <span className="rounded-full bg-yellow-500/15 border border-yellow-500/30 px-2 py-0.5 text-[9px] font-bold text-yellow-400">BETA</span>}
|
||||
</div>
|
||||
</div>
|
||||
<p className="font-semibold text-sm">{t.name}</p>
|
||||
<p className="mt-1 text-xs text-foreground/50 leading-snug">{t.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Inline tools */}
|
||||
{active && (
|
||||
<section className="container mx-auto max-w-3xl px-4 mb-10">
|
||||
<h2 className="font-orbitron text-lg font-bold mb-4">
|
||||
{LAB_TOOLS.find((t) => t.id === active)?.name}
|
||||
</h2>
|
||||
{active === HASH && <HashStation />}
|
||||
{active === BASE && <BaseConverter />}
|
||||
{active === ENTROPY && <EntropyGauge />}
|
||||
{active !== HASH && active !== BASE && active !== ENTROPY && (
|
||||
<div className="rounded-xl border border-zinc-700/40 bg-black/40 p-8 text-center">
|
||||
<p className="text-foreground/50 mb-4">This tool requires VOID credits to access.</p>
|
||||
<Link href="/tools" className="rounded-full bg-neon-purple/20 border border-neon-purple/40 px-6 py-2.5 text-sm font-bold text-neon-purple hover:bg-neon-purple/30">
|
||||
Unlock in VOID Tools →
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section className="container mx-auto max-w-2xl px-4 mt-10 text-center">
|
||||
<div className="rounded-2xl border border-neon-cyan/20 bg-neon-cyan/5 p-8">
|
||||
<h2 className="font-orbitron text-xl font-bold mb-3">Premium labs unlock</h2>
|
||||
<p className="text-sm text-foreground/60 mb-5">Premium tools are included with the VOID Tools subscription. Deposit BTC to get VOID credits.</p>
|
||||
<Link href="/account/add-funds" className="inline-block rounded-full bg-gradient-to-r from-neon-cyan to-neon-purple px-7 py-2.5 text-sm font-bold text-background hover:opacity-90">
|
||||
+ Get VOID Credits
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -11,8 +11,8 @@ export default function LaunchPage() {
|
||||
<p className="font-mono text-[10px] uppercase tracking-[0.35em] text-neon-cyan/80">deployment</p>
|
||||
<h1 className="mt-2 font-orbitron text-4xl font-black text-white md:text-5xl">Launch on the dark web</h1>
|
||||
<p className="mt-4 text-foreground/75">
|
||||
Ship this stack as Tor v3 hidden services: nginx on loopback ports, Next.js on{" "}
|
||||
<code className="rounded bg-white/10 px-1.5 py-0.5 text-neon-green">127.0.0.1:3000</code>, one onion per vertical.
|
||||
Ship this stack as one Tor v3 hidden service: nginx on loopback, Next.js on{" "}
|
||||
<code className="rounded bg-white/10 px-1.5 py-0.5 text-neon-green">127.0.0.1:3000</code>, every vertical as a path.
|
||||
Repo ships generators and systemd hooks — you bring the box and opsec.
|
||||
</p>
|
||||
|
||||
@@ -36,10 +36,10 @@ export default function LaunchPage() {
|
||||
</section>
|
||||
|
||||
<section className="mt-8 space-y-4 rounded-2xl border border-white/10 bg-black/40 p-8 backdrop-blur">
|
||||
<h2 className="font-orbitron text-lg font-bold text-neon-purple">3. Verify every onion & loopback</h2>
|
||||
<h2 className="font-orbitron text-lg font-bold text-neon-purple">3. Verify onion & loopback</h2>
|
||||
<pre className="overflow-x-auto rounded-xl bg-black/60 p-4 text-xs text-neon-cyan/90">
|
||||
{`npm run health:stack
|
||||
npm run onions:list # every .onion URL
|
||||
npm run onions:list # CyberLux .onion URL
|
||||
sudo bash scripts/list-onion-urls.sh # if hostname files need root
|
||||
npm run onions:status # URLs + loopback checks`}
|
||||
</pre>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Suspense, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { ProductSocialBlock } from "@/components/shop/ProductSocialBlock";
|
||||
import { useCart } from "@/contexts/CartContext";
|
||||
@@ -35,7 +35,7 @@ const SYM: Record<ShopCurrency, string> = {
|
||||
|
||||
type SortKey = "relevance" | "price-asc" | "price-desc" | "name";
|
||||
|
||||
export default function MarketPage() {
|
||||
function MarketContent() {
|
||||
const sp = useSearchParams();
|
||||
const router = useRouter();
|
||||
const initialCat = sp.get("category") ?? "";
|
||||
@@ -191,6 +191,14 @@ export default function MarketPage() {
|
||||
);
|
||||
}
|
||||
|
||||
export default function MarketPage() {
|
||||
return (
|
||||
<Suspense fallback={<div className="font-mono text-sm text-[#00ff41]/50">Loading market…</div>}>
|
||||
<MarketContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
function MarketProductCard({
|
||||
product: p,
|
||||
spotBtc,
|
||||
|
||||
150
app/nexus/page.tsx
Normal file
150
app/nexus/page.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Navbar from "@/components/Navbar";
|
||||
|
||||
const NODES = [
|
||||
{ id: "n1", name: "Void Exchange", onion: "voidexxxxx…onion", category: "Market", status: "up", uptime: "99.1%", last: "12s ago", guard: true },
|
||||
{ id: "n2", name: "Phantom Library", onion: "phantomll…onion", category: "Wiki", status: "up", uptime: "97.4%", last: "2m ago", guard: false },
|
||||
{ id: "n3", name: "Ghost Relay #7", onion: "ghostrly7…onion", category: "Relay", status: "up", uptime: "99.8%", last: "5s ago", guard: true },
|
||||
{ id: "n4", name: "Ember Forum", onion: "emberfrum…onion", category: "Forum", status: "down", uptime: "81.2%", last: "4h ago", guard: false },
|
||||
{ id: "n5", name: "Cipher Drop", onion: "ciphdrpxx…onion", category: "Drop", status: "up", uptime: "95.5%", last: "30s ago", guard: false },
|
||||
{ id: "n6", name: "Zero Wiki", onion: "zerowikxx…onion", category: "Wiki", status: "degraded", uptime: "91.0%", last: "8m ago", guard: true },
|
||||
{ id: "n7", name: "Dark Market", onion: "drkmarket…onion", category: "Market", status: "up", uptime: "98.3%", last: "18s ago", guard: true },
|
||||
{ id: "n8", name: "Nexus Gate", onion: "nexusgte…onion", category: "Relay", status: "up", uptime: "99.9%", last: "2s ago", guard: true },
|
||||
];
|
||||
|
||||
const STATUS_COLORS: Record<string, string> = {
|
||||
up: "text-neon-green bg-neon-green/10 border-neon-green/30",
|
||||
down: "text-red-400 bg-red-500/10 border-red-500/30",
|
||||
degraded: "text-yellow-400 bg-yellow-500/10 border-yellow-500/30",
|
||||
};
|
||||
const STATUS_DOT: Record<string, string> = {
|
||||
up: "bg-neon-green",
|
||||
down: "bg-red-500",
|
||||
degraded: "bg-yellow-400",
|
||||
};
|
||||
|
||||
const CATS = ["All", "Market", "Wiki", "Forum", "Relay", "Drop"];
|
||||
|
||||
export default function NexusPage() {
|
||||
const [cat, setCat] = useState("All");
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const filtered = NODES.filter((n) => {
|
||||
const matchCat = cat === "All" || n.category === cat;
|
||||
const matchSearch = !search || n.name.toLowerCase().includes(search.toLowerCase()) || n.onion.includes(search.toLowerCase());
|
||||
return matchCat && matchSearch;
|
||||
});
|
||||
|
||||
const up = NODES.filter((n) => n.status === "up").length;
|
||||
const down = NODES.filter((n) => n.status === "down").length;
|
||||
const degraded = NODES.filter((n) => n.status === "degraded").length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<main className="min-h-screen bg-[#0a0a0a] pt-24 pb-20">
|
||||
<section className="container mx-auto max-w-6xl px-4 mb-10">
|
||||
<p className="mb-2 font-mono text-[10px] uppercase tracking-[0.35em] text-neon-cyan/70">nexus · hidden service network tracker</p>
|
||||
<h1 className="font-orbitron text-4xl font-bold md:text-5xl">
|
||||
THE <span className="text-neon-cyan">NEXUS</span>
|
||||
</h1>
|
||||
<p className="mt-3 max-w-xl text-foreground/60">
|
||||
Real-time monitoring of trusted hidden services across the Tor network. Submit your node to join the monitored ring.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* Status overview */}
|
||||
<section className="container mx-auto max-w-6xl px-4 mb-8">
|
||||
<div className="flex flex-wrap gap-4">
|
||||
{[
|
||||
{ label: "Online", count: up, color: "neon-green" },
|
||||
{ label: "Degraded", count: degraded, color: "yellow-400" },
|
||||
{ label: "Offline", count: down, color: "red-400" },
|
||||
].map((s) => (
|
||||
<div key={s.label} className="flex items-center gap-3 rounded-xl border border-zinc-700/40 bg-black/40 px-5 py-3">
|
||||
<span className={`h-2.5 w-2.5 rounded-full bg-${s.color} animate-pulse`} />
|
||||
<div>
|
||||
<p className="text-xl font-bold font-orbitron">{s.count}</p>
|
||||
<p className="text-xs text-foreground/40">{s.label}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Filters */}
|
||||
<section className="container mx-auto max-w-6xl px-4 mb-6">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<input
|
||||
className="rounded-lg border border-zinc-700 bg-zinc-900 px-4 py-2 text-sm text-foreground placeholder-foreground/30 w-56 focus:border-neon-cyan/40 outline-none"
|
||||
placeholder="Search nodes…"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{CATS.map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
onClick={() => setCat(c)}
|
||||
className={`rounded-full px-3 py-1.5 text-xs font-semibold border transition ${cat === c ? "border-neon-cyan/50 bg-neon-cyan/10 text-neon-cyan" : "border-zinc-700 bg-zinc-900 text-foreground/60 hover:border-zinc-500"}`}
|
||||
>
|
||||
{c}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Node table */}
|
||||
<section className="container mx-auto max-w-6xl px-4">
|
||||
<div className="overflow-hidden rounded-xl border border-zinc-800">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="border-b border-zinc-800 bg-black/60">
|
||||
<tr>
|
||||
{["Node", "Category", "Onion", "Status", "Uptime", "Last seen", "Guard"].map((h) => (
|
||||
<th key={h} className="px-4 py-3 text-left text-[10px] uppercase tracking-wider text-foreground/40">{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filtered.map((node, i) => (
|
||||
<tr key={node.id} className={`border-b border-zinc-800/50 ${i % 2 === 0 ? "bg-black/20" : "bg-black/40"} hover:bg-white/5 transition`}>
|
||||
<td className="px-4 py-3 font-semibold text-foreground">{node.name}</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className="rounded-full border border-zinc-600 bg-zinc-800 px-2 py-0.5 text-[9px] uppercase tracking-wider text-zinc-400">{node.category}</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 font-mono text-xs text-foreground/50">{node.onion}</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-[10px] font-bold ${STATUS_COLORS[node.status]}`}>
|
||||
<span className={`h-1.5 w-1.5 rounded-full ${STATUS_DOT[node.status]}`} />
|
||||
{node.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 font-mono text-xs text-foreground/70">{node.uptime}</td>
|
||||
<td className="px-4 py-3 text-xs text-foreground/40">{node.last}</td>
|
||||
<td className="px-4 py-3 text-center">{node.guard ? <span className="text-neon-cyan text-base">✓</span> : <span className="text-foreground/20">—</span>}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Submit */}
|
||||
<section className="container mx-auto max-w-6xl px-4 mt-12">
|
||||
<div className="rounded-2xl border border-neon-cyan/20 bg-neon-cyan/5 p-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h2 className="font-orbitron text-lg font-bold">Submit your node</h2>
|
||||
<p className="text-sm text-foreground/60 mt-1">Add your hidden service to the Nexus monitoring ring. Requires a signed verification message.</p>
|
||||
</div>
|
||||
<button className="shrink-0 rounded-lg border border-neon-cyan/40 bg-neon-cyan/10 px-5 py-2.5 text-sm font-bold text-neon-cyan hover:bg-neon-cyan/20 transition">
|
||||
Submit Node →
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
134
app/oracle/page.tsx
Normal file
134
app/oracle/page.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import Navbar from "@/components/Navbar";
|
||||
|
||||
const LISTINGS = [
|
||||
{ id: "1", title: "Verified exit scam early warning — active market", category: "Market Intel", price: 8, verified: true, age: "2h ago", rep: 94 },
|
||||
{ id: "2", title: "Opsec hole in popular forum software (CVE-pending)", category: "Vulnerability", price: 25, verified: true, age: "6h ago", rep: 88 },
|
||||
{ id: "3", title: "Darknet exchange front-running scheme — evidence package", category: "Fraud Intel", price: 15, verified: false, age: "1d ago", rep: 72 },
|
||||
{ id: "4", title: "Law enforcement infrastructure node IPs (EU)", category: "Counter-Intel", price: 40, verified: true, age: "2d ago", rep: 91 },
|
||||
{ id: "5", title: "Compromised vendor PGP key — do not transact", category: "Crypto", price: 5, verified: true, age: "3h ago", rep: 99 },
|
||||
{ id: "6", title: "Tor relay operator correlation attack proof-of-concept", category: "Network", price: 30, verified: false, age: "5d ago", rep: 65 },
|
||||
];
|
||||
|
||||
const CATS = ["All", "Market Intel", "Vulnerability", "Fraud Intel", "Counter-Intel", "Crypto", "Network"];
|
||||
|
||||
export default function OraclePage() {
|
||||
const [cat, setCat] = useState("All");
|
||||
|
||||
const filtered = cat === "All" ? LISTINGS : LISTINGS.filter((l) => l.category === cat);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<main className="min-h-screen bg-[#0a0a0a] pt-24 pb-20">
|
||||
{/* Header */}
|
||||
<section className="container mx-auto max-w-6xl px-4 mb-12">
|
||||
<p className="mb-2 font-mono text-[10px] uppercase tracking-[0.35em] text-neon-purple/70">
|
||||
the oracle · verified intelligence market
|
||||
</p>
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
|
||||
<div>
|
||||
<h1 className="font-orbitron text-4xl font-bold md:text-5xl">
|
||||
THE <span className="text-neon-purple">ORACLE</span>
|
||||
</h1>
|
||||
<p className="mt-3 max-w-xl text-foreground/60">
|
||||
Buy and sell verified darknet intelligence. Listings are staked — submitters put VOID credits at risk. Community verification ensures accuracy.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<Link href="/sign-in" className="rounded-lg border border-neon-purple/40 bg-neon-purple/10 px-4 py-2 text-sm font-bold text-neon-purple hover:bg-neon-purple/20">
|
||||
Submit intel →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Stats bar */}
|
||||
<section className="container mx-auto max-w-6xl px-4 mb-8">
|
||||
<div className="flex flex-wrap gap-6 rounded-xl border border-zinc-800 bg-black/40 p-4 text-sm">
|
||||
{[
|
||||
{ label: "Active listings", value: LISTINGS.length },
|
||||
{ label: "Verified", value: LISTINGS.filter((l) => l.verified).length },
|
||||
{ label: "Avg price", value: `✦ ${Math.round(LISTINGS.reduce((a, l) => a + l.price, 0) / LISTINGS.length)} VOID` },
|
||||
{ label: "Avg rep score", value: `${Math.round(LISTINGS.reduce((a, l) => a + l.rep, 0) / LISTINGS.length)}%` },
|
||||
].map((s) => (
|
||||
<div key={s.label}>
|
||||
<p className="text-xs text-foreground/40 uppercase tracking-wider">{s.label}</p>
|
||||
<p className="font-bold text-foreground">{s.value}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Category filter */}
|
||||
<section className="container mx-auto max-w-6xl px-4 mb-6">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{CATS.map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
onClick={() => setCat(c)}
|
||||
className={`rounded-full px-4 py-1.5 text-xs font-semibold border transition ${
|
||||
cat === c
|
||||
? "border-neon-purple/60 bg-neon-purple/20 text-neon-purple"
|
||||
: "border-zinc-700 bg-zinc-900 text-foreground/60 hover:border-zinc-500"
|
||||
}`}
|
||||
>
|
||||
{c}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Listings */}
|
||||
<section className="container mx-auto max-w-6xl px-4">
|
||||
<div className="space-y-3">
|
||||
{filtered.map((listing) => (
|
||||
<div
|
||||
key={listing.id}
|
||||
className="flex flex-col gap-3 rounded-xl border border-zinc-700/40 bg-black/40 p-5 sm:flex-row sm:items-center hover:border-zinc-500 transition cursor-pointer"
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="rounded-full border border-zinc-600 bg-zinc-800 px-2 py-0.5 text-[9px] uppercase tracking-wider text-zinc-400">
|
||||
{listing.category}
|
||||
</span>
|
||||
{listing.verified && (
|
||||
<span className="rounded-full border border-neon-cyan/30 bg-neon-cyan/10 px-2 py-0.5 text-[9px] uppercase tracking-wider text-neon-cyan">
|
||||
✓ verified
|
||||
</span>
|
||||
)}
|
||||
<span className="text-[10px] text-foreground/30">{listing.age}</span>
|
||||
</div>
|
||||
<p className="font-semibold text-foreground text-sm leading-snug">{listing.title}</p>
|
||||
<div className="mt-1 flex items-center gap-3">
|
||||
<span className="text-xs text-foreground/40">Rep: <span className={listing.rep >= 85 ? "text-neon-green" : listing.rep >= 65 ? "text-yellow-500" : "text-red-400"}>{listing.rep}%</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 shrink-0">
|
||||
<span className="font-mono text-base font-bold text-neon-purple">✦ {listing.price}</span>
|
||||
<button className="rounded-lg border border-neon-purple/40 bg-neon-purple/10 px-4 py-1.5 text-xs font-bold text-neon-purple hover:bg-neon-purple/20 transition">
|
||||
Purchase
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Submit box */}
|
||||
<section className="container mx-auto max-w-3xl px-4 mt-16">
|
||||
<div className="rounded-2xl border border-neon-purple/20 bg-neon-purple/5 p-8 text-center">
|
||||
<h2 className="font-orbitron text-xl font-bold mb-2">Have actionable intelligence?</h2>
|
||||
<p className="text-sm text-foreground/60 mb-5">Submit a listing. Stake VOID credits to guarantee accuracy — payouts on verified buys.</p>
|
||||
<Link href="/sign-in" className="inline-block rounded-full bg-neon-purple/20 border border-neon-purple/40 px-6 py-2.5 text-sm font-bold text-neon-purple hover:bg-neon-purple/30">
|
||||
Submit Intelligence →
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,76 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import SecretLayer from "@/components/SecretLayer";
|
||||
import DDoSProtection from "@/components/DDoSProtection";
|
||||
import OnionDisclosureBar from "@/components/OnionDisclosureBar";
|
||||
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 */
|
||||
}
|
||||
}
|
||||
import RaffleCountdownBanner from "@/components/RaffleCountdownBanner";
|
||||
|
||||
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(() => {
|
||||
if (readDdosVerified()) {
|
||||
setIsProtected(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleComplete = useCallback(() => {
|
||||
persistDdosVerified();
|
||||
setIsProtected(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isProtected ? (
|
||||
<DDoSProtection onComplete={handleComplete} />
|
||||
) : (
|
||||
<>
|
||||
<AppProviders>
|
||||
<div className="flex min-h-screen flex-col">
|
||||
<div className="flex-1">{children}</div>
|
||||
<DarkWebLaunchFooter />
|
||||
</div>
|
||||
{/* Must stay inside AppProviders — uses useAccount() */}
|
||||
<HubChatterPublicStrip />
|
||||
</AppProviders>
|
||||
<SecretLayer />
|
||||
<OnionDisclosureBar />
|
||||
</>
|
||||
)}
|
||||
<AppProviders>
|
||||
<div className="flex min-h-screen flex-col">
|
||||
{/* Raffle countdown sticky bar — visible on every page */}
|
||||
<RaffleCountdownBanner />
|
||||
<div className="flex-1">{children}</div>
|
||||
<DarkWebLaunchFooter />
|
||||
</div>
|
||||
{/* Must stay inside AppProviders — uses useAccount() */}
|
||||
<HubChatterPublicStrip />
|
||||
</AppProviders>
|
||||
<SecretLayer />
|
||||
<OnionDisclosureBar />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { FormEvent, useEffect, useState } from "react";
|
||||
import { FormEvent, Suspense, useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useAccount } from "@/contexts/AccountContext";
|
||||
|
||||
export default function SignInPage() {
|
||||
function SignInContent() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { user, hydrated, signIn } = useAccount();
|
||||
@@ -113,3 +113,17 @@ export default function SignInPage() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SignInPage() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex min-h-[50vh] items-center justify-center font-mono text-sm text-zinc-500">
|
||||
Loading…
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<SignInContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { FormEvent, useEffect, useState } from "react";
|
||||
import { FormEvent, Suspense, useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useAccount } from "@/contexts/AccountContext";
|
||||
|
||||
export default function SignUpPage() {
|
||||
function SignUpContent() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { user, hydrated, signUp } = useAccount();
|
||||
@@ -127,3 +127,17 @@ export default function SignUpPage() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SignUpPage() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex min-h-[50vh] items-center justify-center font-mono text-sm text-zinc-500">
|
||||
Loading…
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<SignUpContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
334
app/tools/page.tsx
Normal file
334
app/tools/page.tsx
Normal file
@@ -0,0 +1,334 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import Link from "next/link";
|
||||
import Navbar from "@/components/Navbar";
|
||||
import { useAccount } from "@/contexts/AccountContext";
|
||||
import { TOOLS_CATALOG, CATEGORY_LABELS, type Tool, type ToolCategory } from "@/lib/toolsCatalog";
|
||||
|
||||
type ServerBalance = {
|
||||
voidCredits: number;
|
||||
unlockedToolIds: string[];
|
||||
};
|
||||
|
||||
const CATEGORY_ORDER: ToolCategory[] = ["opsec", "crypto", "network", "intel", "comms", "identity"];
|
||||
|
||||
const CATEGORY_ICONS: Record<ToolCategory, string> = {
|
||||
opsec: "🛡️",
|
||||
crypto: "🔐",
|
||||
network: "🌐",
|
||||
intel: "🔍",
|
||||
comms: "📡",
|
||||
identity: "🪪",
|
||||
};
|
||||
|
||||
function VoidBadge({ credits }: { credits: number }) {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 rounded-full border border-neon-purple/40 bg-neon-purple/10 px-3 py-1 font-mono text-sm text-neon-purple">
|
||||
✦ {credits.toLocaleString()} VOID
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function ToolCard({
|
||||
tool,
|
||||
unlocked,
|
||||
voidBalance,
|
||||
onUnlock,
|
||||
unlocking,
|
||||
}: {
|
||||
tool: Tool;
|
||||
unlocked: boolean;
|
||||
voidBalance: number;
|
||||
onUnlock: (tool: Tool) => void;
|
||||
unlocking: boolean;
|
||||
}) {
|
||||
const canAfford = voidBalance >= tool.cost;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`relative flex flex-col rounded-xl border p-5 transition-all duration-200 ${
|
||||
unlocked
|
||||
? "border-neon-cyan/40 bg-neon-cyan/5"
|
||||
: tool.comingSoon
|
||||
? "border-zinc-700/40 bg-black/30 opacity-60"
|
||||
: canAfford
|
||||
? "border-zinc-600/40 bg-black/40 hover:border-neon-purple/50"
|
||||
: "border-zinc-700/40 bg-black/30"
|
||||
}`}
|
||||
>
|
||||
{unlocked && (
|
||||
<div className="absolute right-3 top-3 rounded-full bg-neon-cyan/15 px-2 py-0.5 text-[10px] font-bold tracking-widest text-neon-cyan">
|
||||
UNLOCKED
|
||||
</div>
|
||||
)}
|
||||
{tool.comingSoon && !unlocked && (
|
||||
<div className="absolute right-3 top-3 rounded-full bg-zinc-700/50 px-2 py-0.5 text-[10px] font-bold tracking-widest text-zinc-400">
|
||||
SOON
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-3 flex items-start gap-3">
|
||||
<span className="text-3xl">{tool.icon}</span>
|
||||
<div className="min-w-0">
|
||||
<p className="font-mono text-[10px] uppercase tracking-widest text-neon-purple/60">
|
||||
{CATEGORY_ICONS[tool.category]} {CATEGORY_LABELS[tool.category]}
|
||||
</p>
|
||||
<h3 className="font-orbitron mt-0.5 text-base font-bold text-foreground">{tool.name}</h3>
|
||||
<p className="mt-0.5 text-xs text-foreground/50">{tool.tagline}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="mb-4 flex-1 text-sm text-foreground/70 leading-relaxed">{tool.description}</p>
|
||||
|
||||
<ul className="mb-5 space-y-1">
|
||||
{tool.features.map((f) => (
|
||||
<li key={f} className="flex items-center gap-2 text-xs text-foreground/55">
|
||||
<span className="text-neon-cyan/70">›</span> {f}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div className="mt-auto flex items-center justify-between gap-3">
|
||||
<span className="font-mono text-sm font-bold text-neon-purple">
|
||||
✦ {tool.cost} VOID
|
||||
</span>
|
||||
{unlocked ? (
|
||||
<Link
|
||||
href={`/tools/${tool.id}`}
|
||||
className="rounded-lg bg-neon-cyan/15 px-4 py-2 text-xs font-bold text-neon-cyan transition hover:bg-neon-cyan/25"
|
||||
>
|
||||
Open →
|
||||
</Link>
|
||||
) : tool.comingSoon ? (
|
||||
<button disabled className="cursor-not-allowed rounded-lg bg-zinc-700/40 px-4 py-2 text-xs text-zinc-500">
|
||||
Coming soon
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => onUnlock(tool)}
|
||||
disabled={!canAfford || unlocking}
|
||||
className={`rounded-lg px-4 py-2 text-xs font-bold transition ${
|
||||
canAfford
|
||||
? "bg-neon-purple/20 text-neon-purple hover:bg-neon-purple/35 border border-neon-purple/30"
|
||||
: "cursor-not-allowed bg-zinc-800/50 text-zinc-500 border border-zinc-700/30"
|
||||
}`}
|
||||
>
|
||||
{unlocking ? "Unlocking…" : canAfford ? "Unlock" : "Need more VOID"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ToolsPage() {
|
||||
const { user, hydrated } = useAccount();
|
||||
const [balance, setBalance] = useState<ServerBalance | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [unlocking, setUnlocking] = useState<string | null>(null);
|
||||
const [toast, setToast] = useState<{ msg: string; ok: boolean } | null>(null);
|
||||
const [filter, setFilter] = useState<ToolCategory | "all">("all");
|
||||
|
||||
const fetchBalance = useCallback(async () => {
|
||||
if (!user?.username) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch(`/api/credits/balance?handle=${encodeURIComponent(user.username)}`);
|
||||
const data = (await res.json()) as { ok: boolean; voidCredits: number; unlockedToolIds: string[] };
|
||||
if (data.ok) setBalance({ voidCredits: data.voidCredits, unlockedToolIds: data.unlockedToolIds });
|
||||
} catch { /* ignore */ } finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [user?.username]);
|
||||
|
||||
useEffect(() => { void fetchBalance(); }, [fetchBalance]);
|
||||
|
||||
const showToast = (msg: string, ok: boolean) => {
|
||||
setToast({ msg, ok });
|
||||
setTimeout(() => setToast(null), 4000);
|
||||
};
|
||||
|
||||
const handleUnlock = async (tool: Tool) => {
|
||||
if (!user?.username || !balance) return;
|
||||
setUnlocking(tool.id);
|
||||
try {
|
||||
const res = await fetch("/api/tools/unlock", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ handle: user.username, toolId: tool.id }),
|
||||
});
|
||||
const data = (await res.json()) as { ok: boolean; error?: string; voidBalance?: number; voidSpent?: number };
|
||||
if (data.ok) {
|
||||
setBalance((prev) =>
|
||||
prev
|
||||
? {
|
||||
voidCredits: data.voidBalance ?? prev.voidCredits - tool.cost,
|
||||
unlockedToolIds: [...prev.unlockedToolIds, tool.id],
|
||||
}
|
||||
: prev,
|
||||
);
|
||||
showToast(`✦ ${tool.name} unlocked! ${tool.cost} VOID spent.`, true);
|
||||
} else {
|
||||
showToast(data.error ?? "Unlock failed", false);
|
||||
}
|
||||
} catch {
|
||||
showToast("Network error", false);
|
||||
} finally {
|
||||
setUnlocking(null);
|
||||
}
|
||||
};
|
||||
|
||||
const filtered = filter === "all" ? TOOLS_CATALOG : TOOLS_CATALOG.filter((t) => t.category === filter);
|
||||
|
||||
if (!hydrated) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<main className="min-h-screen bg-[#0a0a0a] pt-24 pb-20">
|
||||
{/* Header */}
|
||||
<section className="container mx-auto max-w-6xl px-4 mb-12">
|
||||
<p className="mb-2 font-mono text-[10px] uppercase tracking-[0.35em] text-neon-purple/70">
|
||||
access-gated · void credits required
|
||||
</p>
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
|
||||
<div>
|
||||
<h1 className="font-orbitron text-4xl font-bold md:text-5xl">
|
||||
VOID<span className="text-neon-purple"> TOOLS</span>
|
||||
</h1>
|
||||
<p className="mt-3 max-w-xl text-foreground/60">
|
||||
Premium privacy, OPSEC, and intelligence tools. Unlock permanently with VOID credits earned from BTC deposits. No subscriptions — own it forever.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
{user ? (
|
||||
<>
|
||||
<VoidBadge credits={balance?.voidCredits ?? 0} />
|
||||
{loading && <p className="text-xs text-foreground/40">Syncing…</p>}
|
||||
<Link href="/account/add-funds" className="text-xs text-neon-cyan/70 underline hover:text-neon-cyan">
|
||||
+ Deposit BTC for VOID credits
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
<Link
|
||||
href="/sign-in"
|
||||
className="rounded-lg border border-neon-purple/40 bg-neon-purple/10 px-5 py-2.5 text-sm font-bold text-neon-purple hover:bg-neon-purple/20"
|
||||
>
|
||||
Sign in to unlock tools →
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Category filter */}
|
||||
<section className="container mx-auto max-w-6xl px-4 mb-8">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
onClick={() => setFilter("all")}
|
||||
className={`rounded-full px-4 py-1.5 text-xs font-semibold transition border ${
|
||||
filter === "all"
|
||||
? "border-neon-purple/60 bg-neon-purple/20 text-neon-purple"
|
||||
: "border-zinc-700 bg-zinc-900 text-foreground/60 hover:border-zinc-500"
|
||||
}`}
|
||||
>
|
||||
All
|
||||
</button>
|
||||
{CATEGORY_ORDER.map((cat) => (
|
||||
<button
|
||||
key={cat}
|
||||
onClick={() => setFilter(cat)}
|
||||
className={`rounded-full px-4 py-1.5 text-xs font-semibold transition border ${
|
||||
filter === cat
|
||||
? "border-neon-purple/60 bg-neon-purple/20 text-neon-purple"
|
||||
: "border-zinc-700 bg-zinc-900 text-foreground/60 hover:border-zinc-500"
|
||||
}`}
|
||||
>
|
||||
{CATEGORY_ICONS[cat]} {CATEGORY_LABELS[cat]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* How VOID credits work */}
|
||||
{!user && (
|
||||
<section className="container mx-auto max-w-6xl px-4 mb-10">
|
||||
<div className="rounded-xl border border-neon-purple/20 bg-neon-purple/5 p-6">
|
||||
<h2 className="font-orbitron mb-3 text-lg font-bold text-neon-purple">How VOID Credits Work</h2>
|
||||
<div className="grid gap-4 sm:grid-cols-3 text-sm text-foreground/70">
|
||||
<div>
|
||||
<p className="mb-1 font-bold text-foreground">1. Deposit BTC</p>
|
||||
<p>Go to Add Funds. BTCPay generates a unique BTC address. Send any amount.</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="mb-1 font-bold text-foreground">2. Receive VOID</p>
|
||||
<p>After 1 confirmation, credits appear instantly. 1000 sats ≈ 1 VOID credit.</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="mb-1 font-bold text-foreground">3. Unlock Forever</p>
|
||||
<p>Spend VOID to unlock tools permanently. Credits are server-side — work on any device.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Tools grid */}
|
||||
<section className="container mx-auto max-w-6xl px-4">
|
||||
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{filtered.map((tool) => (
|
||||
<ToolCard
|
||||
key={tool.id}
|
||||
tool={tool}
|
||||
unlocked={balance?.unlockedToolIds.includes(tool.id) ?? false}
|
||||
voidBalance={balance?.voidCredits ?? 0}
|
||||
onUnlock={handleUnlock}
|
||||
unlocking={unlocking === tool.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Stats bar */}
|
||||
<section className="container mx-auto max-w-6xl px-4 mt-16">
|
||||
<div className="rounded-xl border border-zinc-800 bg-black/40 p-5 flex flex-wrap gap-6 text-sm">
|
||||
<div>
|
||||
<p className="text-foreground/40 text-xs font-mono mb-1">TOTAL TOOLS</p>
|
||||
<p className="font-bold text-foreground">{TOOLS_CATALOG.filter((t) => !t.comingSoon).length} available</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-foreground/40 text-xs font-mono mb-1">YOUR UNLOCKED</p>
|
||||
<p className="font-bold text-neon-cyan">{balance?.unlockedToolIds.length ?? 0}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-foreground/40 text-xs font-mono mb-1">VOID BALANCE</p>
|
||||
<p className="font-bold text-neon-purple">✦ {(balance?.voidCredits ?? 0).toLocaleString()}</p>
|
||||
</div>
|
||||
<div className="ml-auto flex items-center">
|
||||
<Link
|
||||
href="/account/add-funds"
|
||||
className="rounded-lg bg-gradient-to-r from-neon-cyan to-neon-purple px-5 py-2.5 text-sm font-bold text-background hover:opacity-90"
|
||||
>
|
||||
+ Add VOID Credits
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
{/* Toast */}
|
||||
{toast && (
|
||||
<div
|
||||
className={`fixed bottom-6 right-6 z-50 rounded-xl border px-5 py-3 text-sm font-semibold shadow-xl ${
|
||||
toast.ok
|
||||
? "border-neon-cyan/40 bg-black/90 text-neon-cyan"
|
||||
: "border-red-500/40 bg-black/90 text-red-400"
|
||||
}`}
|
||||
>
|
||||
{toast.msg}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,73 +1,206 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { useAccount } from "@/contexts/AccountContext";
|
||||
|
||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||
|
||||
type Msg = { id: string; from: string; text: string; ts: number; channel?: string };
|
||||
type HandleRecord = {
|
||||
voidCredits: number;
|
||||
claimedInvoices: Record<string, number>;
|
||||
unlockedTools: { toolId: string; unlockedAt: number; voidSpent: number }[];
|
||||
};
|
||||
type LedgerResponse = { ok: boolean; handles: Record<string, HandleRecord>; invoiceIndex: Record<string, string> };
|
||||
type RaffleStats = { ok: boolean; drawAt: number; totalTickets: number; participants: number };
|
||||
type RaffleEntry = { handle: string; tickets: number; boughtAt: number };
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function fmt(ms: number) {
|
||||
if (ms <= 0) return "READY TO DRAW";
|
||||
const s = Math.floor(ms / 1000);
|
||||
const d = Math.floor(s / 86400);
|
||||
const h = Math.floor((s % 86400) / 3600);
|
||||
const m = Math.floor((s % 3600) / 60);
|
||||
const sec = s % 60;
|
||||
if (d > 0) return `${d}d ${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(sec).padStart(2, "0")}`;
|
||||
return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(sec).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
function StatCard({ label, value, sub, accent }: { label: string; value: string | number; sub?: string; accent?: string }) {
|
||||
return (
|
||||
<div className="rounded-lg border border-[#00ff66]/20 bg-black/50 p-4">
|
||||
<div className="text-[9px] uppercase tracking-widest text-zinc-500 mb-1">{label}</div>
|
||||
<div className={`text-2xl font-black font-mono tabular-nums ${accent ?? "text-[#00ff66]"}`}>{value}</div>
|
||||
{sub && <div className="text-[10px] text-zinc-600 mt-1">{sub}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type Tab = "overview" | "credits" | "raffle" | "comms" | "logs";
|
||||
|
||||
// ─── Main ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export default function VaultAdminDashboard() {
|
||||
const { user, hydrated, signIn } = useAccount();
|
||||
const [btcUsd, setBtcUsd] = useState<number | null>(null);
|
||||
const [messages, setMessages] = useState<Msg[]>([]);
|
||||
const [broadcast, setBroadcast] = useState("");
|
||||
const [terminalLines, setTerminalLines] = useState<string[]>([]);
|
||||
const [tab, setTab] = useState<Tab>("overview");
|
||||
const [loginPass, setLoginPass] = useState("");
|
||||
const [loginError, setLoginError] = useState("");
|
||||
|
||||
// BTC
|
||||
const [btcUsd, setBtcUsd] = useState<number | null>(null);
|
||||
|
||||
// Ledger
|
||||
const [handles, setHandles] = useState<Record<string, HandleRecord>>({});
|
||||
const [invoiceIndex, setInvoiceIndex] = useState<Record<string, string>>({});
|
||||
|
||||
// Raffle
|
||||
const [raffleStats, setRaffleStats] = useState<RaffleStats | null>(null);
|
||||
const [raffleEntries, setRaffleEntries] = useState<RaffleEntry[]>([]);
|
||||
const [raffleRemaining, setRaffleRemaining] = useState(0);
|
||||
const [drawResult, setDrawResult] = useState<string | null>(null);
|
||||
|
||||
// Credit ops
|
||||
const [creditHandle, setCreditHandle] = useState("");
|
||||
const [creditAmt, setCreditAmt] = useState("");
|
||||
const [creditMsg, setCreditMsg] = useState<{ text: string; ok: boolean } | null>(null);
|
||||
|
||||
// Comms
|
||||
const [messages, setMessages] = useState<Msg[]>([]);
|
||||
const [broadcast, setBroadcast] = useState("");
|
||||
|
||||
// Logs
|
||||
const [terminalLines, setTerminalLines] = useState<string[]>([
|
||||
`[BOOT] CyberLux admin shell initialised.`,
|
||||
`[AUTH] drjones authenticated via local session.`,
|
||||
]);
|
||||
const logRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// ── Fetchers ────────────────────────────────────────────────────────────────
|
||||
|
||||
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",
|
||||
});
|
||||
const j = await res.json();
|
||||
setBtcUsd(j.bitcoin?.usd || null);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
const r = await fetch("https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd", { cache: "no-store" });
|
||||
const j = await r.json() as { bitcoin?: { usd?: number } };
|
||||
setBtcUsd(j.bitcoin?.usd ?? null);
|
||||
} catch { /* ignore */ }
|
||||
}, []);
|
||||
|
||||
const fetchLedger = useCallback(async () => {
|
||||
try {
|
||||
const r = await fetch("/api/admin/ledger");
|
||||
if (!r.ok) return;
|
||||
const d = await r.json() as LedgerResponse;
|
||||
if (d.ok) { setHandles(d.handles); setInvoiceIndex(d.invoiceIndex); }
|
||||
} catch { /* ignore */ }
|
||||
}, []);
|
||||
|
||||
const fetchRaffle = useCallback(async () => {
|
||||
try {
|
||||
const r = await fetch("/api/raffle/buy");
|
||||
const d = await r.json() as RaffleStats;
|
||||
if (d.ok) setRaffleStats(d);
|
||||
const r2 = await fetch("/api/admin/raffle");
|
||||
if (r2.ok) {
|
||||
const d2 = await r2.json() as { ok: boolean; entries: RaffleEntry[] };
|
||||
if (d2.ok) setRaffleEntries(d2.entries);
|
||||
}
|
||||
} catch { /* 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
|
||||
}
|
||||
const r = await fetch("/api/messages?channel=all");
|
||||
const d = await r.json() as { ok: boolean; messages: Msg[] };
|
||||
if (d.ok) setMessages(d.messages);
|
||||
} catch { /* ignore */ }
|
||||
}, []);
|
||||
|
||||
const log = useCallback((line: string) => {
|
||||
setTerminalLines((prev) => {
|
||||
const next = [...prev.slice(-99), line];
|
||||
return next;
|
||||
});
|
||||
setTimeout(() => { logRef.current?.scrollTo({ top: 9999, behavior: "smooth" }); }, 50);
|
||||
}, []);
|
||||
|
||||
// ── Init ────────────────────────────────────────────────────────────────────
|
||||
|
||||
useEffect(() => {
|
||||
if (!user || user.username !== "drjones") return;
|
||||
void fetchBtc();
|
||||
void fetchLedger();
|
||||
void fetchRaffle();
|
||||
void fetchMessages();
|
||||
const id = setInterval(() => {
|
||||
void fetchBtc();
|
||||
void fetchMessages();
|
||||
}, 10_000);
|
||||
const id = setInterval(() => { void fetchBtc(); void fetchLedger(); void fetchRaffle(); void fetchMessages(); }, 15_000);
|
||||
return () => clearInterval(id);
|
||||
}, [fetchBtc, fetchMessages]);
|
||||
}, [user, fetchBtc, fetchLedger, fetchRaffle, fetchMessages]);
|
||||
|
||||
// Raffle countdown
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
const logs = [
|
||||
`[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)]!]);
|
||||
}, 3500);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
if (!raffleStats) return;
|
||||
const t = setInterval(() => setRaffleRemaining(raffleStats.drawAt - Date.now()), 1000);
|
||||
return () => clearInterval(t);
|
||||
}, [raffleStats]);
|
||||
|
||||
// Simulated terminal feed
|
||||
useEffect(() => {
|
||||
if (!user || user.username !== "drjones") return;
|
||||
const lines = [
|
||||
() => `[TOR] Circuit ping ${Math.floor(Math.random() * 80 + 20)}ms — relay healthy`,
|
||||
() => `[NGINX] 127.0.0.1:8080 → next:3000 — ${Math.floor(Math.random() * 8 + 1)} req/s`,
|
||||
() => `[SHIELD] Null-routing clearnet probe from ${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}.x.x`,
|
||||
() => `[BTC] Block height ${895000 + Math.floor(Math.random() * 500)} confirmed`,
|
||||
() => `[LEDGER] Balances consistent — ${Object.keys(handles).length} handles tracked`,
|
||||
];
|
||||
const t = setInterval(() => log(lines[Math.floor(Math.random() * lines.length)]!()), 4000);
|
||||
return () => clearInterval(t);
|
||||
}, [user, handles, log]);
|
||||
|
||||
// ── Actions ──────────────────────────────────────────────────────────────────
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoginError("");
|
||||
const res = await signIn("drjones", loginPass);
|
||||
if (!res.ok) {
|
||||
setLoginError(res.error || "Access Denied");
|
||||
if (!res.ok) setLoginError(res.error ?? "Access Denied");
|
||||
};
|
||||
|
||||
const adminCredit = async () => {
|
||||
const amt = parseFloat(creditAmt);
|
||||
if (!creditHandle.trim() || isNaN(amt) || amt <= 0) {
|
||||
setCreditMsg({ text: "Handle and positive amount required", ok: false });
|
||||
return;
|
||||
}
|
||||
setCreditMsg(null);
|
||||
try {
|
||||
const r = await fetch("/api/admin/credit", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ handle: creditHandle.trim(), amount: Math.floor(amt) }),
|
||||
});
|
||||
const d = await r.json() as { ok: boolean; newBalance?: number; error?: string };
|
||||
if (d.ok) {
|
||||
setCreditMsg({ text: `✓ ${Math.floor(amt)} VOID → @${creditHandle.trim()} (new balance: ${d.newBalance})`, ok: true });
|
||||
log(`[ADMIN] Manual credit: +${Math.floor(amt)} VOID → @${creditHandle.trim()}`);
|
||||
void fetchLedger();
|
||||
} else {
|
||||
setCreditMsg({ text: d.error ?? "Failed", ok: false });
|
||||
}
|
||||
} catch { setCreditMsg({ text: "Network error", ok: false }); }
|
||||
};
|
||||
|
||||
const doDraw = () => {
|
||||
if (raffleEntries.length === 0) { setDrawResult("No entries yet."); return; }
|
||||
const pool: string[] = [];
|
||||
for (const e of raffleEntries) {
|
||||
for (let i = 0; i < e.tickets; i++) pool.push(e.handle);
|
||||
}
|
||||
const winner = pool[Math.floor(Math.random() * pool.length)]!;
|
||||
setDrawResult(`🎉 WINNER: @${winner} (${raffleEntries.find((e) => e.handle === winner)?.tickets ?? 1} tickets)`);
|
||||
log(`[RAFFLE] Manual draw executed — winner: @${winner}`);
|
||||
};
|
||||
|
||||
const handleBroadcast = async () => {
|
||||
@@ -76,168 +209,449 @@ export default function VaultAdminDashboard() {
|
||||
await fetch("/api/messages", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ from: "System Admin", text: `[GLOBAL BROADCAST] ${broadcast}`, channel: "global" }),
|
||||
body: JSON.stringify({ from: "System Admin", text: `[BROADCAST] ${broadcast}`, channel: "global" }),
|
||||
});
|
||||
setTerminalLines((prev) => [...prev, `[ADMIN] GLOBAL BROADCAST SENT: ${broadcast}`]);
|
||||
log(`[COMMS] Global broadcast sent: "${broadcast}"`);
|
||||
setBroadcast("");
|
||||
void fetchMessages();
|
||||
} catch {
|
||||
setTerminalLines((prev) => [...prev, `[ADMIN] Broadcast Failed!`]);
|
||||
}
|
||||
} catch { log("[COMMS] Broadcast failed!"); }
|
||||
};
|
||||
|
||||
const deleteMessage = async (id: string) => {
|
||||
await fetch(`/api/messages?id=${id}`, { method: "DELETE" });
|
||||
log(`[COMMS] Deleted message ${id}`);
|
||||
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;
|
||||
const nukeMessages = async () => {
|
||||
if (!confirm("Nuke all comms?")) return;
|
||||
await fetch(`/api/messages?all=true`, { method: "DELETE" });
|
||||
log("[COMMS] All messages purged.");
|
||||
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>;
|
||||
}
|
||||
// ── Auth gate ────────────────────────────────────────────────────────────────
|
||||
|
||||
// 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>
|
||||
if (!hydrated) return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-[#050000] text-[#00ff66] font-mono text-sm">
|
||||
Initializing…
|
||||
</div>
|
||||
);
|
||||
|
||||
<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-[#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 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>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="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>
|
||||
</div>
|
||||
|
||||
<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 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 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)]"
|
||||
>
|
||||
EXECUTE OVERRIDE
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<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 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-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>
|
||||
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-sm rounded-xl border border-[#00ff66]/25 bg-black/80 p-8 shadow-[0_0_40px_rgba(0,255,102,0.12)]">
|
||||
<div className="mb-6 text-center">
|
||||
<div className="text-[10px] uppercase tracking-[0.4em] text-[#00ff66]/40 mb-2">CYBERLUX NODE</div>
|
||||
<h1 className="text-2xl font-black uppercase tracking-widest text-red-500">RESTRICTED</h1>
|
||||
<p className="mt-2 text-[10px] text-zinc-600">Operator access only</p>
|
||||
</div>
|
||||
<form onSubmit={handleLogin} className="flex flex-col gap-4">
|
||||
<input
|
||||
type="password"
|
||||
value={loginPass}
|
||||
onChange={(e) => setLoginPass(e.target.value)}
|
||||
className="w-full rounded border border-[#00ff66]/25 bg-black px-4 py-3 text-[#00ff66] text-sm placeholder:text-zinc-700 focus:border-[#00ff66]/60 focus:outline-none"
|
||||
placeholder="passphrase"
|
||||
autoFocus
|
||||
/>
|
||||
{loginError && <div className="text-xs text-red-500">{loginError}</div>}
|
||||
<button type="submit" className="w-full rounded bg-[#00ff66]/15 py-3 text-xs font-bold uppercase tracking-widest text-[#00ff66] hover:bg-[#00ff66]/25 transition-colors">
|
||||
AUTHORIZE
|
||||
</button>
|
||||
</form>
|
||||
<Link href="/" className="mt-6 block text-center text-[10px] uppercase text-zinc-700 hover:text-zinc-500 transition-colors">
|
||||
← Return to hub
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Computed stats ───────────────────────────────────────────────────────────
|
||||
|
||||
const allHandles = Object.entries(handles);
|
||||
const totalVoidCirculating = allHandles.reduce((s, [, r]) => s + r.voidCredits, 0);
|
||||
const totalInvoices = Object.keys(invoiceIndex).length;
|
||||
const totalToolUnlocks = allHandles.reduce((s, [, r]) => s + r.unlockedTools.length, 0);
|
||||
|
||||
const TABS: { id: Tab; label: string; icon: string }[] = [
|
||||
{ id: "overview", label: "Overview", icon: "◈" },
|
||||
{ id: "credits", label: "Credit Ops", icon: "◆" },
|
||||
{ id: "raffle", label: "Raffle", icon: "🎟" },
|
||||
{ id: "comms", label: "Comms", icon: "📡" },
|
||||
{ id: "logs", label: "System Log", icon: "▸" },
|
||||
];
|
||||
|
||||
// ── Render ────────────────────────────────────────────────────────────────────
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#030008] font-mono text-[#00ff66]">
|
||||
{/* Header */}
|
||||
<header className="border-b border-[#00ff66]/20 bg-black/60 backdrop-blur px-6 py-3 flex items-center justify-between gap-4 sticky top-0 z-40">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="inline-block h-2 w-2 rounded-full bg-[#00ff66] shadow-[0_0_8px_#00ff66] animate-pulse" />
|
||||
<span className="text-sm font-black uppercase tracking-widest text-white">CYBERLUX COMMAND</span>
|
||||
<span className="hidden sm:inline text-[10px] text-zinc-600 uppercase tracking-widest">// OPERATOR CONSOLE</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-[10px] uppercase text-zinc-500">
|
||||
<span>@drjones</span>
|
||||
<span className="text-[#00ff66]">GOD MODE</span>
|
||||
{btcUsd && <span className="text-yellow-500">BTC ${btcUsd.toLocaleString()}</span>}
|
||||
<Link href="/" className="rounded border border-zinc-800 px-2 py-1 hover:border-zinc-600 transition-colors">← Hub</Link>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Tab bar */}
|
||||
<nav className="border-b border-[#00ff66]/10 bg-black/40 px-6 flex gap-1">
|
||||
{TABS.map((t) => (
|
||||
<button
|
||||
key={t.id}
|
||||
onClick={() => setTab(t.id)}
|
||||
className={`px-4 py-3 text-xs font-bold uppercase tracking-wider transition-colors border-b-2 ${
|
||||
tab === t.id
|
||||
? "border-[#00ff66] text-[#00ff66]"
|
||||
: "border-transparent text-zinc-600 hover:text-zinc-400"
|
||||
}`}
|
||||
>
|
||||
{t.icon} {t.label}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Body */}
|
||||
<main className="p-6 max-w-7xl mx-auto">
|
||||
|
||||
{/* ── OVERVIEW ─────────────────────────────────────────────────────── */}
|
||||
{tab === "overview" && (
|
||||
<div className="space-y-6">
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4">
|
||||
<StatCard label="VOID Circulating" value={totalVoidCirculating} sub="across all handles" />
|
||||
<StatCard label="Handles" value={allHandles.length} sub="registered" />
|
||||
<StatCard label="Invoices" value={totalInvoices} sub="total created" />
|
||||
<StatCard label="Tool Unlocks" value={totalToolUnlocks} sub="all time" />
|
||||
<StatCard label="BTC / USD" value={btcUsd ? `$${btcUsd.toLocaleString()}` : "…"} sub="live oracle" accent="text-yellow-400" />
|
||||
</div>
|
||||
|
||||
{/* Raffle snapshot */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<StatCard label="Raffle Tickets" value={raffleStats?.totalTickets ?? 0} sub="this round" accent="text-purple-400" />
|
||||
<StatCard label="Participants" value={raffleStats?.participants ?? 0} sub="this round" accent="text-purple-400" />
|
||||
<div className="rounded-lg border border-purple-500/20 bg-black/50 p-4">
|
||||
<div className="text-[9px] uppercase tracking-widest text-zinc-500 mb-1">Next Draw</div>
|
||||
<div className="text-2xl font-black font-mono tabular-nums text-purple-400">
|
||||
{raffleStats ? fmt(raffleStats.drawAt - Date.now()) : "…"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Top handles by VOID */}
|
||||
<div className="rounded-xl border border-[#00ff66]/20 bg-black/50 p-5">
|
||||
<h2 className="text-xs font-bold uppercase tracking-widest mb-4 text-[#00ff66]">Top VOID Holders</h2>
|
||||
{allHandles.length === 0 ? (
|
||||
<div className="text-xs text-zinc-600">No handles registered yet.</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{[...allHandles].sort((a, b) => b[1].voidCredits - a[1].voidCredits).slice(0, 10).map(([handle, rec]) => (
|
||||
<div key={handle} className="flex items-center gap-3">
|
||||
<span className="text-xs text-zinc-400 w-32 truncate">@{handle}</span>
|
||||
<div className="flex-1 h-2 rounded-full bg-zinc-900">
|
||||
<div
|
||||
className="h-2 rounded-full bg-[#00ff66]/60"
|
||||
style={{ width: `${Math.min(100, (rec.voidCredits / Math.max(1, totalVoidCirculating)) * 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs font-bold text-[#00ff66] w-16 text-right tabular-nums">{rec.voidCredits} V</span>
|
||||
<span className="text-[10px] text-zinc-600 w-20 text-right">{rec.unlockedTools.length} tools</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Quick ops */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
{[
|
||||
{ label: "VOID Tools", href: "/tools", desc: "Tool marketplace", icon: "🔓" },
|
||||
{ label: "DarkHost", href: "/hosting", desc: "Hosting panel", icon: "🖥️" },
|
||||
{ label: "Add Funds", href: "/account/add-funds", desc: "BTC deposit", icon: "₿" },
|
||||
].map((l) => (
|
||||
<Link key={l.href} href={l.href} className="flex items-center gap-3 rounded-lg border border-zinc-800 bg-black/30 p-4 hover:border-[#00ff66]/40 transition-colors">
|
||||
<span className="text-xl">{l.icon}</span>
|
||||
<div>
|
||||
<div className="text-xs font-bold text-white">{l.label}</div>
|
||||
<div className="text-[10px] text-zinc-600">{l.desc}</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── CREDIT OPS ───────────────────────────────────────────────────── */}
|
||||
{tab === "credits" && (
|
||||
<div className="space-y-6">
|
||||
{/* Manual credit */}
|
||||
<div className="rounded-xl border border-[#00ff66]/25 bg-black/50 p-6">
|
||||
<h2 className="text-xs font-bold uppercase tracking-widest mb-4 text-[#00ff66]">Manual VOID Credit</h2>
|
||||
<div className="flex flex-wrap gap-3 items-end">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[10px] uppercase text-zinc-500">Handle</label>
|
||||
<input
|
||||
value={creditHandle}
|
||||
onChange={(e) => setCreditHandle(e.target.value)}
|
||||
placeholder="username"
|
||||
className="rounded border border-zinc-800 bg-black px-3 py-2 text-sm text-[#00ff66] placeholder:text-zinc-700 focus:border-[#00ff66]/50 focus:outline-none w-44"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[10px] uppercase text-zinc-500">VOID Amount</label>
|
||||
<input
|
||||
value={creditAmt}
|
||||
onChange={(e) => setCreditAmt(e.target.value)}
|
||||
placeholder="50"
|
||||
type="number"
|
||||
min="1"
|
||||
className="rounded border border-zinc-800 bg-black px-3 py-2 text-sm text-[#00ff66] placeholder:text-zinc-700 focus:border-[#00ff66]/50 focus:outline-none w-32"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={adminCredit}
|
||||
className="rounded bg-[#00ff66]/15 border border-[#00ff66]/30 px-5 py-2 text-xs font-bold uppercase text-[#00ff66] hover:bg-[#00ff66]/25 transition-colors"
|
||||
>
|
||||
CREDIT
|
||||
</button>
|
||||
</div>
|
||||
{creditMsg && (
|
||||
<div className={`mt-3 text-xs ${creditMsg.ok ? "text-[#00ff66]" : "text-red-400"}`}>
|
||||
{creditMsg.text}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Full ledger table */}
|
||||
<div className="rounded-xl border border-[#00ff66]/20 bg-black/50 p-5">
|
||||
<h2 className="text-xs font-bold uppercase tracking-widest mb-4 text-[#00ff66]">
|
||||
Full VOID Ledger — {allHandles.length} handles
|
||||
</h2>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="text-[10px] uppercase text-zinc-600 border-b border-zinc-800">
|
||||
<th className="text-left py-2 pr-4">Handle</th>
|
||||
<th className="text-right py-2 pr-4">VOID</th>
|
||||
<th className="text-right py-2 pr-4">Invoices</th>
|
||||
<th className="text-right py-2 pr-4">Tools</th>
|
||||
<th className="text-right py-2">Spent (VOID)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{allHandles.length === 0 ? (
|
||||
<tr><td colSpan={5} className="py-6 text-center text-zinc-600">No handles yet.</td></tr>
|
||||
) : (
|
||||
[...allHandles]
|
||||
.sort((a, b) => b[1].voidCredits - a[1].voidCredits)
|
||||
.map(([handle, rec]) => {
|
||||
const spent = rec.unlockedTools.reduce((s, t) => s + t.voidSpent, 0);
|
||||
return (
|
||||
<tr key={handle} className="border-b border-zinc-900 hover:bg-white/2 transition-colors">
|
||||
<td className="py-2 pr-4 text-white font-bold">@{handle}</td>
|
||||
<td className="py-2 pr-4 text-right tabular-nums text-[#00ff66] font-bold">{rec.voidCredits}</td>
|
||||
<td className="py-2 pr-4 text-right tabular-nums text-zinc-400">{Object.keys(rec.claimedInvoices).length}</td>
|
||||
<td className="py-2 pr-4 text-right tabular-nums text-purple-400">{rec.unlockedTools.length}</td>
|
||||
<td className="py-2 text-right tabular-nums text-zinc-500">{spent}</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Invoice index */}
|
||||
<div className="rounded-xl border border-zinc-800 bg-black/40 p-5">
|
||||
<h2 className="text-xs font-bold uppercase tracking-widest mb-4 text-zinc-500">
|
||||
Invoice Index — {totalInvoices} total
|
||||
</h2>
|
||||
<div className="overflow-x-auto max-h-48 overflow-y-auto">
|
||||
<table className="w-full text-[10px]">
|
||||
<thead>
|
||||
<tr className="text-zinc-700 border-b border-zinc-900 uppercase">
|
||||
<th className="text-left py-1 pr-4">Invoice ID</th>
|
||||
<th className="text-left py-1">Handle</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.entries(invoiceIndex).slice(-30).reverse().map(([inv, handle]) => (
|
||||
<tr key={inv} className="border-b border-zinc-900/60">
|
||||
<td className="py-1 pr-4 font-mono text-zinc-500">{inv.slice(0, 20)}…</td>
|
||||
<td className="py-1 text-zinc-400">@{handle}</td>
|
||||
</tr>
|
||||
))}
|
||||
{totalInvoices === 0 && (
|
||||
<tr><td colSpan={2} className="py-4 text-center text-zinc-700">No invoices yet.</td></tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── RAFFLE ───────────────────────────────────────────────────────── */}
|
||||
{tab === "raffle" && (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
||||
<StatCard label="Tickets Sold" value={raffleStats?.totalTickets ?? 0} accent="text-purple-400" />
|
||||
<StatCard label="Participants" value={raffleStats?.participants ?? 0} accent="text-purple-400" />
|
||||
<div className="rounded-lg border border-purple-500/20 bg-black/50 p-4 col-span-2">
|
||||
<div className="text-[9px] uppercase tracking-widest text-zinc-500 mb-1">Time to Draw</div>
|
||||
<div className="text-3xl font-black font-mono tabular-nums text-purple-400">{fmt(raffleRemaining)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Draw control */}
|
||||
<div className="rounded-xl border border-purple-500/30 bg-[#0d0010]/60 p-6">
|
||||
<h2 className="text-xs font-bold uppercase tracking-widest mb-4 text-purple-400">Manual Draw</h2>
|
||||
<p className="text-xs text-zinc-500 mb-4">
|
||||
Executes a weighted random draw client-side for preview purposes. Winner is selected proportional to ticket count.
|
||||
</p>
|
||||
<button
|
||||
onClick={doDraw}
|
||||
className="rounded bg-purple-900/40 border border-purple-500/40 px-6 py-2 text-sm font-bold text-purple-300 hover:bg-purple-900/60 transition-colors"
|
||||
>
|
||||
🎲 EXECUTE DRAW
|
||||
</button>
|
||||
{drawResult && (
|
||||
<div className="mt-4 rounded-lg border border-purple-500/30 bg-purple-900/20 p-4 text-sm font-bold text-purple-300">
|
||||
{drawResult}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Entries */}
|
||||
<div className="rounded-xl border border-[#00ff66]/20 bg-black/50 p-5">
|
||||
<h2 className="text-xs font-bold uppercase tracking-widest mb-4 text-[#00ff66]">Current Entries</h2>
|
||||
{raffleEntries.length === 0 ? (
|
||||
<div className="text-xs text-zinc-600 py-4">No entries yet.</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{[...raffleEntries].sort((a, b) => b.tickets - a.tickets).map((e) => {
|
||||
const pct = raffleStats ? Math.round((e.tickets / raffleStats.totalTickets) * 100) : 0;
|
||||
return (
|
||||
<div key={e.handle} className="flex items-center gap-3">
|
||||
<span className="text-xs text-zinc-400 w-32 truncate">@{e.handle}</span>
|
||||
<div className="flex-1 h-2 rounded-full bg-zinc-900">
|
||||
<div className="h-2 rounded-full bg-purple-500/60" style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
<span className="text-xs font-bold text-purple-300 w-16 text-right tabular-nums">{e.tickets} tickets</span>
|
||||
<span className="text-[10px] text-zinc-600 w-10 text-right">{pct}%</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── COMMS ────────────────────────────────────────────────────────── */}
|
||||
{tab === "comms" && (
|
||||
<div className="space-y-6">
|
||||
{/* Broadcast */}
|
||||
<div className="rounded-xl border border-red-600/30 bg-[#1a0000]/50 p-6">
|
||||
<h2 className="text-xs font-bold uppercase tracking-widest mb-3 text-red-500">Global Network Broadcast</h2>
|
||||
<textarea
|
||||
value={broadcast}
|
||||
onChange={(e) => setBroadcast(e.target.value)}
|
||||
className="mb-3 h-20 w-full resize-none rounded border border-red-900/40 bg-black/60 p-3 text-xs text-red-300 placeholder:text-red-900 focus:border-red-600/50 focus:outline-none"
|
||||
placeholder="Message to all connected nodes…"
|
||||
/>
|
||||
<button
|
||||
onClick={handleBroadcast}
|
||||
className="rounded bg-red-900/50 border border-red-700/50 px-6 py-2 text-xs font-bold uppercase text-white hover:bg-red-800/60 transition-colors shadow-[0_0_10px_rgba(255,0,0,0.2)]"
|
||||
>
|
||||
EXECUTE BROADCAST
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Message feed */}
|
||||
<div className="rounded-xl border border-[#00ff66]/20 bg-black/50 p-5">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-xs font-bold uppercase tracking-widest text-[#00ff66]">
|
||||
Comms Intercept — {messages.length} messages
|
||||
</h2>
|
||||
<button
|
||||
onClick={nukeMessages}
|
||||
className="rounded border border-red-900/40 bg-red-900/20 px-3 py-1 text-[10px] font-bold text-red-500 hover:bg-red-900/40 transition-colors"
|
||||
>
|
||||
PURGE ALL
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-2 max-h-96 overflow-y-auto pr-1">
|
||||
{messages.length === 0 ? (
|
||||
<div className="py-8 text-center text-xs text-zinc-600">No traffic detected.</div>
|
||||
) : (
|
||||
[...messages].reverse().map((msg) => (
|
||||
<div key={msg.id} className="rounded bg-[#0a110d] border border-[#00ff66]/10 p-3">
|
||||
<div className="flex items-center justify-between mb-1 flex-wrap gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-bold text-[#00ff66]">@{msg.from}</span>
|
||||
<span className="text-[9px] text-zinc-600 bg-black px-1.5 rounded">#{msg.channel ?? "global"}</span>
|
||||
<span className="text-[9px] text-zinc-700">{new Date(msg.ts).toLocaleString()}</span>
|
||||
</div>
|
||||
<button onClick={() => deleteMessage(msg.id)} className="text-[10px] text-red-500 hover:text-red-400 font-bold">
|
||||
[DEL]
|
||||
</button>
|
||||
</div>
|
||||
<div className="text-sm text-white/85 font-sans break-words">{msg.text}</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── LOGS ─────────────────────────────────────────────────────────── */}
|
||||
{tab === "logs" && (
|
||||
<div className="rounded-xl border border-[#00ff66]/20 bg-black/80 p-5">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-xs font-bold uppercase tracking-widest text-[#00ff66]">System Log</h2>
|
||||
<button
|
||||
onClick={() => setTerminalLines([])}
|
||||
className="text-[10px] text-zinc-600 hover:text-zinc-400 transition-colors"
|
||||
>
|
||||
CLEAR
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
ref={logRef}
|
||||
className="h-[60vh] overflow-y-auto rounded bg-[#020005] border border-white/5 p-4 text-[11px] leading-relaxed space-y-0.5"
|
||||
>
|
||||
{terminalLines.map((line, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={
|
||||
line.includes("[ADMIN]") ? "text-red-400" :
|
||||
line.includes("[RAFFLE]") ? "text-purple-400" :
|
||||
line.includes("[COMMS]") ? "text-yellow-400" :
|
||||
line.includes("[SHIELD]") || line.includes("[AUTH]") ? "text-orange-400" :
|
||||
line.includes("[BTC]") ? "text-yellow-300" :
|
||||
"text-[#00ff66]/70"
|
||||
}
|
||||
>
|
||||
<span className="text-zinc-700 select-none">{String(i + 1).padStart(4, " ")} │ </span>
|
||||
{line}
|
||||
</div>
|
||||
))}
|
||||
<div className="text-[#00ff66] animate-pulse">_</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user