"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 = { opsec: "🛡️", crypto: "🔐", network: "🌐", intel: "🔍", comms: "📡", identity: "🪪", }; function VoidBadge({ credits }: { credits: number }) { return ( ✦ {credits.toLocaleString()} VOID ); } 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 (
{unlocked && (
UNLOCKED
)} {tool.comingSoon && !unlocked && (
SOON
)}
{tool.icon}

{CATEGORY_ICONS[tool.category]} {CATEGORY_LABELS[tool.category]}

{tool.name}

{tool.tagline}

{tool.description}

✦ {tool.cost} VOID {unlocked ? ( Open → ) : tool.comingSoon ? ( ) : ( )}
); } export default function ToolsPage() { const { user, hydrated } = useAccount(); const [balance, setBalance] = useState(null); const [loading, setLoading] = useState(false); const [unlocking, setUnlocking] = useState(null); const [toast, setToast] = useState<{ msg: string; ok: boolean } | null>(null); const [filter, setFilter] = useState("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 ( <>
{/* Header */}

access-gated · void credits required

VOID TOOLS

Premium privacy, OPSEC, and intelligence tools. Unlock permanently with VOID credits earned from BTC deposits. No subscriptions — own it forever.

{user ? ( <> {loading &&

Syncing…

} + Deposit BTC for VOID credits ) : ( Sign in to unlock tools → )}
{/* Category filter */}
{CATEGORY_ORDER.map((cat) => ( ))}
{/* How VOID credits work */} {!user && (

How VOID Credits Work

1. Deposit BTC

Go to Add Funds. BTCPay generates a unique BTC address. Send any amount.

2. Receive VOID

After 1 confirmation, credits appear instantly. 1000 sats ≈ 1 VOID credit.

3. Unlock Forever

Spend VOID to unlock tools permanently. Credits are server-side — work on any device.

)} {/* Tools grid */}
{filtered.map((tool) => ( ))}
{/* Stats bar */}

TOTAL TOOLS

{TOOLS_CATALOG.filter((t) => !t.comingSoon).length} available

YOUR UNLOCKED

{balance?.unlockedToolIds.length ?? 0}

VOID BALANCE

✦ {(balance?.voidCredits ?? 0).toLocaleString()}

+ Add VOID Credits
{/* Toast */} {toast && (
{toast.msg}
)} ); }