Compare commits

...

8 Commits

Author SHA1 Message Date
drjones
e7c9da944e Replace remote with local repo 2026-04-26 22:28:40 -07:00
drjones
04d64fb993 Update market, account, and onion operations
Capture the current CyberLux UI, commerce, messaging, and Tor ops updates so local main can be pushed to the remote.

Made-with: Cursor
2026-04-24 23:23:48 -07:00
drjones
cb072437d0 Add ONION-URLS.md, export script, and tighten README
- ONION-URLS.md: static registry of all 43 hidden services (port, role, app path)
- scripts/export-onion-urls.sh: writes live .onion addresses to onion-urls.txt at runtime
- npm run onions:export shortcut added to package.json
- onion-urls.txt added to .gitignore (runtime artifact, not source)
- README: cohesive rewrite — architecture diagram, operator command table, source-of-truth
  map, BTCPay notes, full site map, common fixes
- BTCPay status route: awaited params destructuring (Next 15 async params)

Made-with: Cursor
2026-04-16 18:03:30 -07:00
drjones
4354416f1c Integrate BTCPay Server: auto-settling invoice deposits
- lib/btcpay.ts: Greenfield API client (createBtcPayInvoice, getBtcPayInvoiceStatus)
- /api/btcpay/invoice: creates per-deposit invoice (USD amount + handle in metadata)
- /api/btcpay/status/[id]: polls invoice status + returns BTC address/amount
- add-funds page: BTCPay tab with amount picker, live address display,
  10s auto-poll, settlement auto-credits localStorage ledger
- WalletContext: creditBtcPayInvoice() with duplicate-invoice guard
- .env.example: BTCPAY_URL, BTCPAY_API_KEY, BTCPAY_STORE_ID, NEXT_PUBLIC_BTCPAY_ENABLED

Made-with: Cursor
2026-04-16 01:23:35 -07:00
drjones
348e55b63e Add proper deposit desk: multi-coin, clear custody model, DepositWidget
- /account/add-funds: full rebuild as Deposit Desk
  - 3-step how-it-works explainer: you send crypto → we hold it →
    you shop with USD credit → we pay vendors with your crypto
  - Coin selector: BTC (auto-verify), ETH (manual review), XMR (manual review)
  - Each coin shows deposit address from env var with copy button
  - BTC: immediate on-chain verify via /api/btc/verify (mempool.space)
  - ETH/XMR: submit txhash to /api/pending-deposit for manual review
  - Pending deposit history stored in localStorage with status
  - Important notes block explaining custody, no-withdrawal, confirmations
  - Back-links to checkout, wallets, dashboard

- /api/pending-deposit: new route to log ETH/XMR manual review requests
  (logs to console, ready to wire to DB/webhook)

- DepositWidget component: reusable compact + full variants
  - compact: balance + "Deposit crypto →" CTA (used in checkout)
  - full: balance + how-it-works summary (used in dashboard)

- Dashboard: replace balance section with DepositWidget + stats
- Checkout: add compact DepositWidget above CheckoutFlow, fix back link
  from "Sanctuary" → "Market", update feature cards to be accurate

- .env.example: document all three coin address env vars + optional
  payment processor URL

- .env.example: add NEXT_PUBLIC_ETH_ADDRESS, NEXT_PUBLIC_XMR_ADDRESS

Made-with: Cursor
2026-04-16 01:03:14 -07:00
drjones
2f928fbdc4 10x every page: real interactions, kill fake content, wire everything
- ChatWidget: remove illegal seeds, real localStorage per-handle chat,
  honest bot replies about market/forum/funds
- ForumBoard: wire to real forumState (loadForum/addThread/vote), kill
  fake stats and illegal seed posts
- Home page: privacy features list reflects reality, footer links real
- Links: kill all alert() calls, replace fake onions with real clearnet
  privacy resources + internal route grid
- Support: per-coin copied state, env-driven addresses, real BTC addr
- Inner circle: wire to AccountContext, tier system from LUX balance,
  remove hardcoded admin/shadow credentials and fake trading signals
- Drop box: real sealed-note localStorage system, honest about no
  anonymous upload capability, real file picker with receipt
- Messages: fully functional per-handle localStorage chat, AI-style
  contextual bot replies, clear history, honest about local storage
- Wallets: pivot from fake PayPal accounts to Digital Access Passes,
  wire Buy Now to cart via ShopProduct interface
- Testimonials: wire submit form to localStorage, interactive star
  rating 1-10, display submitted reviews above the fold
- Raffle: use real merchant BTC address, real per-handle entry storage,
  honest LUX-only prize disclaimer, fix 0x address
- Drops/Lotto: real number picker 1-49 with Quick Pick, ticket
  submission, match display against drawn numbers, demo disclaimer
- Sanctuary: real 4-4-6-2 breathing timer, meditation passage with
  timer, candle-lighting with localStorage notes
- Game: full playable Void Pong with canvas physics, CPU AI, scoring,
  rally counter, localStorage high score
- Security analysis: honest architecture breakdown with real grades,
  layer-by-layer analysis, practical OPSEC guide, fiction banner
- Trust: compute real scores from actual localStorage data (LUX,
  USD, forum posts, testimonials), FAQ accordion

Made-with: Cursor
2026-04-16 00:55:28 -07:00
drjones
9da373a190 README: manifesto voice, final product copy
Lead with thesis and declaration; keep operator table and ops intact.
Closing line marks README as product intent.

Made-with: Cursor
2026-04-15 22:14:05 -07:00
drjones
e0a4a20862 Tor ops: launch UI, onion list/health scripts, systemd installer, README glow-up
- Replace fake-site footer with dark-web launch CTA; fix HubChatter inside AppProviders (client crash)
- Add /launch, DEPLOY.md, onions:list|status|health, list-onion-urls, install-systemd, onion-status
- start.sh CYBERLUX_PREPARE_ONLY; copy pass on satellite pages; Navbar launch link
- README: operator cheat sheet, phone Tor note, systemd via install-systemd.sh

Made-with: Cursor
2026-04-15 22:12:44 -07:00
124 changed files with 10873 additions and 6447 deletions

14
.gitignore vendored
View File

@@ -39,3 +39,17 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
# runtime-generated onion URL file (populated by scripts/export-onion-urls.sh)
onion-urls.txt
# runtime data
.messages.json
/logs/
# vanity onion mining (mkp224o output + upstream clone)
/vendor/mkp224o/
/var/vanity-mine/
# server-side credit ledger (runtime state — do not commit)
/var/server-ledger.json

129
DEPLOY.md Normal file
View File

@@ -0,0 +1,129 @@
# CyberLux — production on Tor (.onion)
This app is designed to run **behind nginx on loopback**, with **one Tor v3 hidden service for the whole site**. Nginx listens on `127.0.0.1:8080` (see `scripts/onion-nodes.json`); Tor forwards port 80 on the onion to that loopback port. **Next.js binds only `127.0.0.1:3000`** — never expose 3000 or the nginx loopback port to the public internet.
Onion hostnames are **created locally** when Tor first starts; there are no fixed `.onion` URLs in the repo. Back up `/var/lib/tor/cyberlux/hs_ed25519_secret_key` (the install flow uses `scripts/backup-onion-keys.sh`). A vanity prefix such as `cyberlux...onion` requires generating or importing a matching Tor v3 hidden-service key.
## One-time server setup (Debian/Ubuntu-style)
```bash
sudo apt update
sudo apt install -y tor nginx curl nodejs npm build-essential
# Or install Node.js LTS from NodeSource / nvm — `node` and `npm` must be on PATH.
```
## Deploy the app
From the repo root (as the user that will own the process):
```bash
./start.sh
```
This will: regenerate Tor/nginx maps from `scripts/onion-nodes.json`, `npm install`, `npm run build`, install Tor+nginx configs (`sudo`), wait for the hostname file, print the `.onion` URL, then **foreground** `next start` on `127.0.0.1:3000`.
For a **one-shot prepare** (build + Tor/nginx, no Next.js — for systemd):
```bash
CYBERLUX_PREPARE_ONLY=1 ./start.sh
```
## systemd — start on boot
1. Ensure `./start.sh` or `CYBERLUX_PREPARE_ONLY=1 ./start.sh` has been run at least once so `.next` exists and Tor directories are populated.
2. Install the unit (run as root; set user to the account that owns the repo):
```bash
sudo CYBERLUX_USER=youruser bash scripts/install-systemd.sh
# optional: sudo CYBERLUX_CHOWN_REPO=1 CYBERLUX_USER=youruser bash scripts/install-systemd.sh
```
3. Enable **Tor**, **nginx**, and **CyberLux** at boot:
```bash
sudo systemctl enable tor.service nginx.service cyberlux.service
# if your distro uses tor@default instead of tor:
# sudo systemctl enable tor@default.service nginx.service cyberlux.service
sudo systemctl start tor.service nginx.service cyberlux.service
```
4. Check logs:
```bash
journalctl -u cyberlux.service -f
```
5. Health check (local):
```bash
npm run health:stack
```
6. **List the `.onion` URL** and **check that the nginx loopback vhost answers** (needs Tor running; use `sudo` if hostname files are root-only):
```bash
npm run onions:status
# or:
sudo node scripts/onion-status.cjs
```
## Changing the onion map
1. Edit `scripts/onion-nodes.json`.
2. Run `node scripts/generate-onion-config.cjs` (or `npm run build`, which runs it in `prebuild`).
3. `sudo bash scripts/install-tor-onion.sh`
4. Rebuild/restart the app: `npm run build` and `sudo systemctl restart cyberlux.service`
## 502 Bad Gateway on `.onion` sites
Tor and nginx are working, but **nginx proxies to Next.js on `127.0.0.1:3000`**. A **502** means **nothing is listening there** (Next is stopped, crashed, or never started after reboot).
1. **Confirm** (from the repo):
```bash
curl -sS -o /dev/null -w "%{http_code}\n" http://127.0.0.1:3000/
```
`000` = connection refused → Next is down.
2. **Start Next** (pick one):
- **Foreground (dev / quick test):** `cd /path/to/cyberlux && npm run start:onion` — leave the terminal open.
- **systemd (production):** `sudo systemctl start cyberlux.service` — ensure the unit is installed (`scripts/install-systemd.sh`) and enabled.
- **Full stack script:** `./start.sh` (builds, configures Tor/nginx if needed, then starts Next).
3. **Verify again:**
```bash
npm run health:stack
```
You want `Next.js: OK` and `hub vhost: OK` (HTTP 200/301/302/304).
4. **If it still fails:** `journalctl -u cyberlux.service -n 80 --no-pager` — look for crash loops, missing `.next` (run `npm run build`), or wrong `WorkingDirectory` in the unit.
## Custom .onion prefix (branded / vanity name)
v3 hostnames are random unless you **mine** a key with [mkp224o](https://github.com/cathugger/mkp224o) and install it. That is a **separate, long-running CPU** step — not part of a normal deploy.
1. `bash scripts/mkp224o-build.sh` (or Docker; see `ONION-URLS.md`)
2. `bash scripts/vanity-onion-mine.sh <base32_prefix>`
3. `sudo bash scripts/install-vanity-onion-key.sh var/vanity-mine`
Full details: [`ONION-URLS.md`](ONION-URLS.md#vanity-onion-custom-prefix-eg-site-name-at-the-start).
## Verification
```bash
npm run verify
```
## Security & compliance
- You are responsible for **local law**, hosting terms, and **Tor / relay policies**. This repo is a **parody web app**; treat operational security seriously if you run it on a real server.
## Security notes
- Only **Tor** should be reachable from outside; bind **nothing** to `0.0.0.0` for this stack.
- Prefer firewall defaults that deny incoming except what you need for SSH.
- Optional: `sudo bash scripts/classroom-ufw.sh` (if present) for a restrictive UFW profile.

135
ONION-URLS.md Normal file
View File

@@ -0,0 +1,135 @@
# CyberLux — Onion URL Reference
CyberLux now uses one Tor v3 hidden service for the whole site. The live `.onion` address is generated at runtime by Tor and stored in `/var/lib/tor/cyberlux/hostname`. This file is a **static registry**; use the commands below to resolve the live address.
---
## Get Live URLs
```bash
# Print the live .onion URL (reads /var/lib/tor/cyberlux/hostname)
bash scripts/list-onion-urls.sh
# or with sudo if not readable as your user:
sudo bash scripts/list-onion-urls.sh
# Save live URLs to onion-urls.txt in the repo root
sudo bash scripts/export-onion-urls.sh
```
---
## Service Registry
| # | `torDir` | Port | Role | App Path |
|---|----------|------|------|----------|
| 1 | `cyberlux` | 8080 | Everything site — hub, market, forum, directory, wiki, account, and all app paths | `/directory` |
---
## Live Address File
After Tor is running, save all live addresses to a plain-text file:
```bash
sudo bash scripts/export-onion-urls.sh
# writes: onion-urls.txt
```
`onion-urls.txt` (if present in this directory) contains the resolved `http://…56chars….onion` address and is gitignored — it reflects runtime state, not source.
---
## How Addresses Stay Stable
The service maps to a fixed `HiddenServiceDir` in `/var/lib/tor/`. As long as:
- the `torDir` name in `scripts/onion-nodes.json` stays `cyberlux`, and
- `/var/lib/tor/cyberlux/` is preserved (or restored from backup),
the `.onion` address **never rotates**, surviving reboots, rebuilds, and Tor restarts.
Backup/restore:
```bash
sudo bash scripts/backup-onion-keys.sh # saves to /var/backups/cyberlux-onion-keys/current
sudo bash scripts/restore-onion-keys.sh # restores missing dirs from backup
```
---
## Vanity .onion (custom prefix, e.g. site name at the start)
v3 onion names are random unless you **generate** a key pair whose address matches a **prefix** (brute force). This repo ships scripts around **[mkp224o](https://github.com/cathugger/mkp224o)** (same approach as the [Tor community vanity guide](https://community.torproject.org/onion-services/advanced/vanity-addresses/)).
**Charset:** Onion v3 hostnames are base32: **az** and **27** only (no `0`, `1`, `8`, `9`). Any other character in the filter is invalid.
**Time:** Shorter prefixes finish sooner; each extra character is much harder. Treat long targets (e.g. full word) as a **long** CPU run or use many cores / a batch-capable build per mkp224os `OPTIMISATION.txt`.
### 1) Build the miner (once)
```bash
# Debian/Ubuntu deps (as root)
sudo apt install -y git gcc make autoconf pkg-config libc6-dev libsodium-dev
cd /path/to/cyberlux
bash scripts/mkp224o-build.sh
# produces: vendor/mkp224o/mkp224o
```
`vendor/mkp224o` is gitignored; nothing is committed except these scripts.
**Docker alternative (no local compile):**
```bash
export CYBERLUX_MKP224O_DOCKER=1
export CYBERLUX_VANITY_OUT="/path/to/cyberlux/var/vanity-mine"
mkdir -p "$CYBERLUX_VANITY_OUT"
bash scripts/vanity-onion-mine.sh yourprefix
```
(Requires Docker; uses `ghcr.io/cathugger/mkp224o:master`.)
### 2) Mine a prefix (runs until a match appears)
```bash
bash scripts/vanity-onion-mine.sh cyberl
# Output directory: var/vanity-mine/ (default) — gitignored
```
You get a new subdirectory named like `cyberlXXXXXXXX.onion` containing `hs_ed25519_secret_key` and `hostname`. **Stop the miner** when you have a directory you are happy with if it created more than one.
### 3) Install the key for CyberLux and restart Tor
`HiddenServiceDir` in this project is `/var/lib/tor/cyberlux` (from `onion-nodes.json`). The install script copies the **files** from the mkp224o folder into that directory, fixes ownership, and restarts Tor.
```bash
# If var/vanity-mine has several *.onion folders, the script picks one for you
# (default: first in AZ order). Newest mtime: CYBERLUX_VANITY_PICK=latest
sudo bash scripts/install-vanity-onion-key.sh var/vanity-mine
sudo env CYBERLUX_VANITY_PICK=latest bash scripts/install-vanity-onion-key.sh var/vanity-mine
# Or point at one directory explicitly:
sudo bash scripts/install-vanity-onion-key.sh var/vanity-mine/cyberlxxxxx....onion
```
The previous service directory is moved to `/var/lib/tor/cyberlux.stale.<timestamp>`. A backup is triggered when `backup-onion-keys.sh` is present.
### 4) Confirm the new URL
```bash
bash scripts/list-onion-urls.sh
# or
sudo cat /var/lib/tor/cyberlux/hostname
```
Open `http://<address>.onion` in **Tor Browser**.
---
## Notes
- The service runs from **one** Next.js instance at `127.0.0.1:3000`.
- nginx on `127.0.0.1:8080` proxies every path to Next.
- Open addresses in **Tor Browser** or **Onion Browser** (iOS). Standard browsers cannot resolve `.onion`.
- Use `http://` — TLS inside the onion circuit is handled end-to-end by Tor.
- A vanity prefix is **not** “configured” in `torrc`; only the private key in `HiddenServiceDir` determines the hostname.

375
README.md
View File

@@ -1,272 +1,235 @@
# CYBERLUX
> Neon storefront. Forty-three onion doors. One build. One app. No dead air.
> **The clearnet is a showroom. The onion is the door. One engine behind every door.**
CyberLux is a layered Next.js fiction stack built for dark-web literacy, classroom demos, mirror discipline drills, and absurdly overbuilt cyberpunk presentation. It runs one app behind Tor + nginx, then fans that app out into dedicated v3 hidden services for the hub, forum, exchange, market, crawler, syndicate, wiki layer, and every other top-level surface in `scripts/onion-nodes.json`.
One Next.js stack projected through a single Tor v3 hidden service. Nginx sits in the middle; Tor does the publishing. Same codebase, same deploy — **one `.onion` front door** with every section available as a normal app path.
This repo is built to feel like a real underground property while still being a controlled demo environment:
---
- `43` Tor v3 services
- stable loopback range `127.0.0.1:80808122`
- Next bound only to `127.0.0.1:3000`
- one `.onion` per major surface
- persistent hidden-service identity keyed by `HiddenServiceDir`
- self-healing backup / restore of onion key directories
## Architecture
## What It Does
```
Tor Browser
│ http://<56-char>.onion
[ Tor Network ]
nginx 127.0.0.1:8080 (single onion vhost)
Next.js 127.0.0.1:3000 (single instance, proxy.ts routes by header)
```
CyberLux boots a single Next.js app and projects it through multiple onion entry points.
**One onion service → one app → zero public ports.**
- `hub` is the main storefront
- `forum` is Void Aggregate
- `exchange` is the classifieds stack
- `market` is the full catalog vertical
- `search` is Void Crawler
- `wiki` is the hidden-wiki layer
- `w` is the syndicate shell network
- dozens of additional dedicated onions map directly to top-level app routes
---
The routing layer uses host-aware rewrites so a dedicated onion feels like its own property without forking the app into 43 separate deployments.
## One-Command Launch
Install system deps first on Debian/Ubuntu:
## Quick Start
```bash
sudo apt install tor nginx
```
Then run:
```bash
chmod +x start.sh
./start.sh
```
What `./start.sh` does:
Open the hub in **Tor Browser** (addresses print at the end of start.sh).
Local-only (skip Tor/nginx): `CYBERLUX_SKIP_TOR=1 ./start.sh`
1. regenerates Tor / nginx / app route maps from `scripts/onion-nodes.json`
2. runs `git pull --ff-only` if the repo has `.git` (failure is non-fatal)
3. runs `npm install`
4. repairs `.next` ownership if a root-owned build left it unwritable
5. runs a production build
6. restores backed-up onion keys if any hidden-service directories are missing
7. installs Tor + nginx config only when it actually changed
8. waits for all onion hostname files, not just the hub
9. refreshes the onion key backup set
10. prints every live `.onion` URL
11. starts Next on `127.0.0.1:3000`
---
Use Tor Browser and open the printed URLs as:
## Operator Commands
```text
http://<56-char-v3-address>.onion
| Command | What it does |
|---------|-------------|
| `./start.sh` | Full pipeline: generate → build → Tor/nginx → print URLs → start Next |
| `npm run onions:list` | Print the live `.onion` URL from `/var/lib/tor/cyberlux/hostname` |
| `sudo npm run onions:export` | Write the live URL to `onion-urls.txt` in repo root |
| `npm run onions:status` | URL + HTTP probe for the nginx vhost |
| `npm run health:stack` | Curl Tor/nginx/Next loopbacks and report status |
| `npm run verify` | Full pre-ship check: config gen, TS, shell syntax, build |
| `sudo bash scripts/install-tor-onion.sh` | Install/refresh Tor + nginx config |
| `sudo bash scripts/backup-onion-keys.sh` | Back up all hidden-service key dirs |
| `sudo bash scripts/restore-onion-keys.sh` | Restore missing key dirs from backup |
| `sudo bash scripts/install-systemd.sh` | Install `cyberlux.service` for boot-time start |
| `sudo bash scripts/classroom-ufw.sh` | Lock firewall to loopback-only (recommended) |
| `bash scripts/mkp224o-build.sh` | Build the mkp224o v3 vanity key generator into `vendor/mkp224o/` (gitignored) |
| `bash scripts/vanity-onion-mine.sh <prefix>` | Brute-force a .onion starting with `prefix` (base32) → `var/vanity-mine/` |
| `sudo bash scripts/install-vanity-onion-key.sh var/vanity-mine` | Install mined keys into `/var/lib/tor/cyberlux`, restart Tor |
**Vanity .onion** (a custom prefix, e.g. the site name) is optional and CPU-heavy. Full steps: [`ONION-URLS.md`](ONION-URLS.md#vanity-onion-custom-prefix-eg-site-name-at-the-start).
---
## Onion URL Reference
See [`ONION-URLS.md`](ONION-URLS.md) — live address notes for the single CyberLux onion.
To export live `.onion` addresses after Tor is running:
```bash
sudo bash scripts/export-onion-urls.sh
# writes onion-urls.txt (gitignored — reflects runtime state)
```
---
## What Ships in the Box
- **1** Tor v3 hidden service
- Loopback port `127.0.0.1:8080` (nginx vhost, never public)
- Next.js bound only to `127.0.0.1:3000`
- One `.onion` for the whole site — hub, forum, market, exchange, wiki, and directory paths
- Persistent hidden-service identity keyed by `HiddenServiceDir` name
- Self-healing backup / restore for onion keys
- BTCPay Server integration (configurable) for on-chain + Lightning deposits
- Systemd unit generator for unattended boot
---
## What `./start.sh` Does (in order)
1. Regenerates Tor / nginx / app route maps from `scripts/onion-nodes.json`
2. Runs `git pull --ff-only` if repo has `.git` (non-fatal on failure)
3. Runs `npm install`
4. Repairs `.next` ownership if a root-owned build left it unwritable
5. Runs a production build
6. Restores backed-up onion keys if any hidden-service directories are missing
7. Installs Tor + nginx config (only when it changed)
8. Waits for the CyberLux onion hostname file
9. Refreshes the onion key backup set
10. Prints every live `.onion` URL
11. Starts Next on `127.0.0.1:3000`
---
## Source of Truth
Everything — Tor config, nginx vhosts, app route maps — is generated from one file:
```
scripts/onion-nodes.json
```
Generated artifacts (do not edit manually):
| File | Description |
|------|-------------|
| `tor/cyberlux-nodes.conf` | Tor `HiddenServiceDir` / `HiddenServicePort` blocks |
| `lib/onionRoutes.generated.ts` | TypeScript route map used by `proxy.ts` |
| `scripts/generated/tor-dirs.txt` | Newline-separated list of service dir names |
| `scripts/generated/onion-labels.tsv` | Human-readable labels per service |
| `scripts/generated/onion-port-range.txt` | Port min / max for scripts |
---
## Persistent Onion Addresses
Yes: the onion addresses are meant to persist.
Addresses are stable as long as:
Each service uses a fixed Tor directory:
```tor
HiddenServiceDir /var/lib/tor/<service-dir>
HiddenServicePort 80 127.0.0.1:<loopback-port>
```
That means the `.onion` address remains stable as long as:
- the `torDir` names in `scripts/onion-nodes.json` do not change
- `/var/lib/tor/<service-dir>` is preserved
- `torDir` names in `scripts/onion-nodes.json` do not change
- `/var/lib/tor/<service-dir>/` is preserved
- or the backup set can restore those directories
Addresses do **not** rotate on normal app restarts, rebuilds, or standard Tor restarts.
Normal app restarts, rebuilds, and standard Tor restarts **do not rotate addresses**.
## Self-Healing Onion Identity
Backup location: `/var/backups/cyberlux-onion-keys/current`
CyberLux now includes automatic onion key persistence tooling:
---
- `scripts/backup-onion-keys.sh`
- `scripts/restore-onion-keys.sh`
## BTCPay Server
Backup location:
The checkout flow supports BTCPay Server for on-chain + Lightning payments. Configure with environment variables (see `.env.example` or `DEPLOY.md`). The stack runs without BTCPay — the API returns `503` if not configured.
```text
/var/backups/cyberlux-onion-keys/current
```
---
Behavior:
- after boot, ready onion service directories are backed up
- on next boot, if a service dir is missing from `/var/lib/tor`, it is restored from backup
- Tor identity is preserved across ordinary host mistakes, partial deletions, or service-dir drift recovery
This is the difference between “looks cool in a screenshot” and “survives operator error.”
## Tor / nginx Topology
Source of truth:
- `scripts/onion-nodes.json`
Generated artifacts:
- `tor/cyberlux-nodes.conf`
- `nginx/cyberlux-onion-servers.inc`
- `lib/onionRoutes.generated.ts`
- `scripts/generated/tor-dirs.txt`
- `scripts/generated/onion-labels.tsv`
- `scripts/generated/onion-port-range.txt`
Install path:
- `/etc/tor/cyberlux-nodes.conf`
- `/etc/nginx/cyberlux-server-common.inc`
- `/etc/nginx/cyberlux-onion-servers.inc`
- `/etc/nginx/sites-available/cyberlux-onion`
Runtime:
- Next.js: `127.0.0.1:3000`
- nginx onion vhosts: `127.0.0.1:80808122`
- Tor exposes the public `.onion` endpoints
## Manual Ops
Install or refresh Tor/nginx config:
## Boot at Startup (systemd)
```bash
sudo bash scripts/install-tor-onion.sh
sudo CYBERLUX_USER=$USER bash scripts/install-systemd.sh
sudo systemctl enable --now tor.service nginx.service cyberlux.service
```
Back up onion keys immediately:
Use `tor@default.service` instead of `tor.service` if your distro names it that way.
```bash
sudo bash scripts/backup-onion-keys.sh
```
Tor + nginx must already be configured (`./start.sh` or `sudo bash scripts/install-tor-onion.sh` first).
Restore missing onion keys:
```bash
sudo bash scripts/restore-onion-keys.sh
```
Run local-only, skipping Tor/nginx:
```bash
CYBERLUX_SKIP_TOR=1 ./start.sh
```
Start app without the helper:
```bash
npm run build
npm run start:onion
```
## Boot At Startup
`scripts/cyberlux.service` is included as a systemd template for the Next process.
Before installing it, set:
- `User=`
- `Group=`
- `WorkingDirectory=`
Then install:
```bash
sudo install -m 0644 scripts/cyberlux.service /etc/systemd/system/cyberlux.service
sudo systemctl daemon-reload
sudo systemctl enable --now cyberlux.service
```
Notes:
- Tor and nginx should already be installed and enabled
- the service unit keeps the Next process alive
- Tor/nginx config is still managed by the repo scripts
---
## Verification
Run the full local verification suite:
```bash
npm run verify
```
That checks:
Checks: config generation, TypeScript compilation, shell syntax for all boot/install/backup/restore scripts, full Next.js production build.
- onion config generation
- TypeScript
- shell syntax for boot / install / backup / restore scripts
- full Next production build
---
## Common Failure: `.next` Permission Hell
## Common Fixes
If you previously ran a build with `sudo`, `.next` may be root-owned and Next will fail with `EACCES` during unlink / diagnostics writes.
Fix once:
**`.next` permission error (built with sudo):**
```bash
sudo bash scripts/fix-next-perms.sh
```
Then rerun:
```bash
./start.sh
```
## Security Notes
**Onion hostnames not showing up:**
CyberLux is hardened for a demo stack, not sold as magic invisibility.
```bash
sudo systemctl status tor
sudo ls -la /var/lib/tor/
# Wait ~30s after Tor starts for v3 descriptor publication
```
- Next and nginx bind to loopback only
- Tor exposes the onion endpoints
- do **not** publish `3000` or `80808122` to the public internet
- optional host firewall script: `sudo bash scripts/classroom-ufw.sh`
- the app can still be insecure if the app code is insecure
- operational mistakes still matter more than branding
**Port 3000 already in use:**
Tor hides service reachability better than raw-IP hosting. It does not redeem bad application logic, bad operator habits, or bad judgment.
```bash
ss -ltnp | grep 3000
# Stop the conflicting process, then ./start.sh
```
---
## Security
- Next.js and nginx bind to **loopback only** — they are never public
- Tor publishes the `.onion` endpoints
- Do **not** expose ports 3000 or 8080 to the internet
- Harden with: `sudo bash scripts/classroom-ufw.sh`
- Tor obscures reachability — it does not forgive bad code or bad ops
Read `DEPLOY.md` before pointing real users at this stack.
---
## Site Map
Core verticals:
| Vertical | Path | Onion |
|---------|------|-------|
| Master directory | `/directory` | `cyberlux` |
| Hub storefront | `/` | `cyberlux` |
| Forum (Void Aggregate) | `/forum` | `cyberlux` |
| Exchange (classifieds) | `/exchange` | `cyberlux` |
| Market (full catalog) | `/market` | `cyberlux` |
| Barter (Ash Pit) | `/barter` | `cyberlux` |
| Search (Void Crawler) | `/search` | `cyberlux` |
| Hidden Wiki | `/hidden-wiki` | `cyberlux` |
| Syndicate (shell net) | `/syndicate` | `cyberlux` |
- `/` hub storefront
- `/market` full catalog
- `/forum` Void Aggregate
- `/exchange` classifieds
- `/barter` Ash Pit
- `/search` Void Crawler
- `/hidden-wiki` wiki layer
- `/darknet-atlas` taxonomy / atlas
- `/syndicate` shell network
- `/dashboard` personal relay
- `/account/hidden-services` mirror map
There are additional dedicated onions for many top-level routes beyond those.
---
## Stack
- Next.js `16`
- React `19`
- Tailwind CSS `4`
- nginx
- Tor hidden services
- generated route / host mapping
- **Next.js 16** · React 19 · Tailwind CSS 4
- **nginx** reverse proxy (loopback vhosts)
- **Tor** v3 hidden services
- **BTCPay Server** (optional, on-chain + Lightning)
- Generated route / host mapping from single JSON source
## Final Word
---
CyberLux is supposed to boot like a machine that knows what it is:
- same onion doors
- same identities
- no shallow mirrors
- no fake “self healing” copy without actual restore logic
- no dead routes pretending to be part of the network
If it starts, it should start hard.
> If you are running this, you are past cosplay.
> The network does not owe you anonymity — **you** owe the network discipline.
> Read `DEPLOY.md`. Own your jurisdiction.

193
app/academy/page.tsx Normal file
View 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>
</>
);
}

View File

@@ -0,0 +1,745 @@
"use client";
import { FormEvent, useCallback, useEffect, useRef, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import Navbar from "@/components/Navbar";
import { useAccount } from "@/contexts/AccountContext";
import { useWallet } from "@/contexts/WalletContext";
import { getMerchantBtcAddress, isMerchantBtcConfigured } from "@/lib/merchantBtc";
// ─── Types ─────────────────────────────────────────────────────────────────
type Tab = "BTCPAY" | "BTC" | "ETH" | "XMR";
type BtcPayInvoiceLocal = {
invoiceId: string;
handle: string;
usdAmount: number;
btcAddress: string | null;
btcAmount: string | null;
btcRate: string | null;
status: "New" | "Processing" | "Expired" | "Invalid" | "Settled";
createdAt: number;
credited: boolean;
};
type ManualDeposit = {
id: string;
coin: "ETH" | "XMR";
txid: string;
ts: number;
status: "pending" | "credited";
ref?: string;
};
// ─── localStorage helpers ───────────────────────────────────────────────────
const INVOICE_KEY = "cyberlux-btcpay-invoices-v1";
const MANUAL_KEY = "cyberlux-pending-deposits-v1";
function loadInvoices(): BtcPayInvoiceLocal[] {
if (typeof window === "undefined") return [];
try { return JSON.parse(localStorage.getItem(INVOICE_KEY) ?? "[]") as BtcPayInvoiceLocal[]; }
catch { return []; }
}
function saveInvoices(d: BtcPayInvoiceLocal[]) {
if (typeof window !== "undefined") localStorage.setItem(INVOICE_KEY, JSON.stringify(d));
}
function loadManual(): ManualDeposit[] {
if (typeof window === "undefined") return [];
try { return JSON.parse(localStorage.getItem(MANUAL_KEY) ?? "[]") as ManualDeposit[]; }
catch { return []; }
}
function saveManual(d: ManualDeposit[]) {
if (typeof window !== "undefined") localStorage.setItem(MANUAL_KEY, JSON.stringify(d));
}
function shortId() { return Math.random().toString(36).slice(2, 10).toUpperCase(); }
// ─── Style maps ────────────────────────────────────────────────────────────
const TAB_COLOR: Record<Tab, string> = {
BTCPAY: "text-neon-purple",
BTC: "text-amber-400",
ETH: "text-blue-400",
XMR: "text-orange-400",
};
const TAB_BORDER: Record<Tab, string> = {
BTCPAY: "border-neon-purple/30",
BTC: "border-amber-500/30",
ETH: "border-blue-500/30",
XMR: "border-orange-500/30",
};
const TAB_BG: Record<Tab, string> = {
BTCPAY: "bg-neon-purple/10",
BTC: "bg-amber-500/10",
ETH: "bg-blue-500/10",
XMR: "bg-orange-500/10",
};
// ─── Component ─────────────────────────────────────────────────────────────
export default function AddFundsPage() {
const router = useRouter();
const { user, hydrated } = useAccount();
const { usdStoreCredit, luxCredits, verifyBtcDeposit, creditBtcPayInvoice } = useWallet();
const btcPayEnabled = (process.env.NEXT_PUBLIC_BTCPAY_ENABLED ?? "").toLowerCase() === "true";
const [activeTab, setActiveTab] = useState<Tab>(btcPayEnabled ? "BTCPAY" : "BTC");
const [copied, setCopied] = useState<Tab | null>(null);
// ── BTCPay state ──────────────────────────────────────────────────────────
const [desiredUsd, setDesiredUsd] = useState("25");
const [bpBusy, setBpBusy] = useState(false);
const [bpError, setBpError] = useState<string | null>(null);
const [activeInvoice, setActiveInvoice] = useState<BtcPayInvoiceLocal | null>(null);
const [invoiceHistory, setInvoiceHistory] = useState<BtcPayInvoiceLocal[]>([]);
const [bpSuccess, setBpSuccess] = useState<string | null>(null);
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
// ── Manual BTC verify state ───────────────────────────────────────────────
const [btcTxid, setBtcTxid] = useState("");
const [btcBusy, setBtcBusy] = useState(false);
const [btcMsg, setBtcMsg] = useState<{ kind: "ok" | "err"; text: string } | null>(null);
// ── ETH / XMR manual state ────────────────────────────────────────────────
const [manualTxid, setManualTxid] = useState("");
const [manualBusy, setManualBusy] = useState(false);
const [manualMsg, setManualMsg] = useState<{ kind: "ok" | "err" | "pending"; text: string; ref?: string } | null>(null);
const [manualHistory, setManualHistory] = useState<ManualDeposit[]>([]);
const btcAddr = getMerchantBtcAddress();
const btcReady = isMerchantBtcConfigured();
const ethAddr = (process.env.NEXT_PUBLIC_ETH_ADDRESS || "").trim();
const xmrAddr = (process.env.NEXT_PUBLIC_XMR_ADDRESS || "").trim();
// ── Auth guard ────────────────────────────────────────────────────────────
useEffect(() => {
if (hydrated && !user) router.replace("/sign-in?next=/account/add-funds");
}, [hydrated, user, router]);
// ── Load local state ──────────────────────────────────────────────────────
useEffect(() => {
setInvoiceHistory(loadInvoices());
setManualHistory(loadManual());
}, []);
// ── Poll active BTCPay invoice ────────────────────────────────────────────
const stopPoll = useCallback(() => {
if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; }
}, []);
const pollInvoice = useCallback(
async (invoice: BtcPayInvoiceLocal) => {
if (invoice.credited || invoice.status === "Settled" || invoice.status === "Expired" || invoice.status === "Invalid") {
stopPoll();
return;
}
try {
const res = await fetch(`/api/btcpay/status/${invoice.invoiceId}`);
const data = (await res.json()) as {
ok: boolean;
status?: string;
usdAmount?: number;
btcAddress?: string | null;
btcAmount?: string | null;
btcRate?: string | null;
settled?: boolean;
error?: string;
};
if (!data.ok) return;
const newStatus = (data.status ?? invoice.status) as BtcPayInvoiceLocal["status"];
// Update address/amount if BTCPay just returned them (first poll after creation)
const updated: BtcPayInvoiceLocal = {
...invoice,
status: newStatus,
btcAddress: data.btcAddress ?? invoice.btcAddress,
btcAmount: data.btcAmount ?? invoice.btcAmount,
btcRate: data.btcRate ?? invoice.btcRate,
credited: invoice.credited,
};
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();
}
if (newStatus === "Expired" || newStatus === "Invalid") stopPoll();
setActiveInvoice(updated);
setInvoiceHistory((prev) => {
const next = prev.map((inv) => inv.invoiceId === updated.invoiceId ? updated : inv);
if (!next.find((i) => i.invoiceId === updated.invoiceId)) next.unshift(updated);
saveInvoices(next);
return next;
});
} catch { /* ignore network blips */ }
},
[user, creditBtcPayInvoice, stopPoll],
);
useEffect(() => {
if (!activeInvoice) { stopPoll(); return; }
if (activeInvoice.credited || activeInvoice.status === "Settled" || activeInvoice.status === "Expired") return;
stopPoll();
pollRef.current = setInterval(() => { void pollInvoice(activeInvoice); }, 10_000);
// also poll immediately to fetch btcAddress on first render
void pollInvoice(activeInvoice);
return stopPoll;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeInvoice?.invoiceId]);
useEffect(() => { return stopPoll; }, [stopPoll]);
// ── Handlers ──────────────────────────────────────────────────────────────
const createInvoice = async (e: FormEvent) => {
e.preventDefault();
if (!user) return;
const usd = parseFloat(desiredUsd);
if (!Number.isFinite(usd) || usd < 0.5) { setBpError("Enter an amount ≥ $0.50"); return; }
setBpBusy(true);
setBpError(null);
setBpSuccess(null);
try {
const res = await fetch("/api/btcpay/invoice", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ usdAmount: usd, handle: user.username }),
});
const data = (await res.json()) as {
ok: boolean;
invoiceId?: string;
checkoutLink?: string;
status?: string;
expiresAt?: number;
error?: string;
};
if (!data.ok || !data.invoiceId) {
setBpError(data.error ?? "Failed to create invoice. Check BTCPay config.");
return;
}
const newInv: BtcPayInvoiceLocal = {
invoiceId: data.invoiceId,
handle: user.username,
usdAmount: usd,
btcAddress: null,
btcAmount: null,
btcRate: null,
status: (data.status ?? "New") as BtcPayInvoiceLocal["status"],
createdAt: Date.now(),
credited: false,
};
setActiveInvoice(newInv);
setInvoiceHistory((prev) => { const n = [newInv, ...prev]; saveInvoices(n); return n; });
} catch {
setBpError("Network error. Is BTCPay reachable?");
} finally {
setBpBusy(false);
}
};
const copyText = async (text: string, tab: Tab) => {
try { await navigator.clipboard.writeText(text); setCopied(tab); setTimeout(() => setCopied(null), 1800); }
catch { /* ignore */ }
};
const onVerifyBtc = async (e: FormEvent) => {
e.preventDefault();
if (!btcTxid.trim()) return;
setBtcBusy(true); setBtcMsg(null);
try {
const res = await verifyBtcDeposit(btcTxid.trim());
if (res.ok) {
setBtcMsg({ kind: "ok", text: `$${(res.creditedUsd ?? 0).toFixed(2)} USD credited to @${user!.username}.` });
setBtcTxid("");
} else {
setBtcMsg({ kind: "err", text: res.error ?? "Verification failed." });
}
} finally { setBtcBusy(false); }
};
const onSubmitManual = async (e: FormEvent) => {
e.preventDefault();
if (!manualTxid.trim() || !user || (activeTab !== "ETH" && activeTab !== "XMR")) return;
setManualBusy(true); setManualMsg(null);
try {
const res = await fetch("/api/pending-deposit", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ coin: activeTab, txid: manualTxid.trim(), handle: user.username }),
});
const json = (await res.json()) as { ok: boolean; message?: string; ref?: string; error?: string };
if (json.ok) {
const entry: ManualDeposit = {
id: shortId(), coin: activeTab as "ETH" | "XMR",
txid: manualTxid.trim(), ts: Date.now(), status: "pending", ref: json.ref,
};
setManualHistory((prev) => { const n = [entry, ...prev]; saveManual(n); return n; });
setManualMsg({ kind: "pending", text: json.message ?? "Submitted for manual review.", ref: json.ref });
setManualTxid("");
} else {
setManualMsg({ kind: "err", text: json.error ?? "Submission failed." });
}
} catch {
setManualMsg({ kind: "err", text: "Network error." });
} finally { setManualBusy(false); }
};
// ── Loading state ─────────────────────────────────────────────────────────
if (!hydrated || !user) {
return (
<div className="flex min-h-screen items-center justify-center bg-[#0a0a0a] font-mono text-sm text-zinc-500">
Loading
</div>
);
}
const tabs: Tab[] = btcPayEnabled ? ["BTCPAY", "BTC", "ETH", "XMR"] : ["BTC", "ETH", "XMR"];
return (
<div className="min-h-screen bg-[#0a0a0a] text-zinc-200">
<Navbar />
<div className="mx-auto max-w-3xl px-4 py-28">
{/* Header */}
<div className="mb-10">
<p className="font-mono text-[10px] uppercase tracking-[0.35em] text-neon-cyan/70">
@{user.username} · deposit desk
</p>
<h1 className="mt-2 font-orbitron text-4xl font-bold">Fund Your Account</h1>
<p className="mt-3 max-w-2xl text-sm text-zinc-400 leading-relaxed">
Send crypto to the address below. We verify the transaction on-chain, convert to USD at current
market rate, and credit your account.{" "}
<strong className="text-zinc-200">You no longer hold the crypto</strong> we do.
When you purchase, we pay the vendor in crypto on your behalf.
</p>
</div>
{/* How it works */}
<div className="mb-8 grid grid-cols-1 gap-4 sm:grid-cols-3">
{[
{ n: "1", icon: "₿", title: "Send crypto", body: "BTC via BTCPay, or BTC/ETH/XMR to a static address." },
{ n: "2", icon: "📊", title: "We verify", body: "On-chain confirmation. Credited at live market USD rate." },
{ n: "3", icon: "🛒", title: "You shop", body: "Spend USD credits site-wide. We pay vendors in crypto." },
].map((s) => (
<div key={s.n} className="glass rounded-2xl border border-white/10 p-5">
<div className="mb-3 flex items-center gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-neon-cyan/15 text-sm font-bold text-neon-cyan">
{s.n}
</div>
<span className="text-xl">{s.icon}</span>
</div>
<div className="font-bold text-zinc-100">{s.title}</div>
<p className="mt-1 text-xs text-zinc-500">{s.body}</p>
</div>
))}
</div>
{/* Balance card */}
<div className="glass mb-8 rounded-2xl border border-neon-cyan/20 p-6">
<div className="flex flex-wrap items-center justify-between gap-4">
<div>
<div className="text-xs text-zinc-500">Current balance @{user.username}</div>
<div className="mt-1 font-orbitron text-3xl text-neon-cyan">${usdStoreCredit.toFixed(2)} USD</div>
<div className="mt-0.5 text-sm text-neon-green">{luxCredits.toLocaleString()} LUX</div>
</div>
<div className="flex gap-3">
<Link href="/checkout" className="rounded-lg border border-neon-cyan/30 px-4 py-2 text-sm text-neon-cyan hover:bg-neon-cyan/10">
Spend
</Link>
<Link href="/wallets" className="rounded-lg border border-white/10 px-4 py-2 text-sm text-zinc-400 hover:border-white/20">
Passes
</Link>
</div>
</div>
</div>
{/* Tab selector */}
<div className="mb-6 flex gap-2 flex-wrap">
{tabs.map((tab) => (
<button
key={tab}
type="button"
onClick={() => { setActiveTab(tab); setBpError(null); setBtcMsg(null); setManualMsg(null); }}
className={`flex-1 min-w-[80px] rounded-xl border py-3 text-xs font-bold uppercase transition-all ${
activeTab === tab
? `${TAB_BORDER[tab]} ${TAB_BG[tab]} ${TAB_COLOR[tab]}`
: "border-white/10 text-zinc-500 hover:border-white/20"
}`}
>
{tab === "BTCPAY" ? "BTCPay ✦" : tab}
</button>
))}
</div>
{/* ── BTCPAY TAB ─────────────────────────────────────────────────── */}
{activeTab === "BTCPAY" && (
<div className="space-y-5">
<div className="glass rounded-2xl border border-neon-purple/30 p-6">
<div className="mb-1 flex items-center justify-between">
<h2 className="font-orbitron text-lg text-neon-purple">BTCPay Invoice</h2>
<span className="rounded-full bg-neon-green/15 px-3 py-1 text-xs font-bold text-neon-green">
Auto-settle
</span>
</div>
<p className="mb-5 text-xs text-zinc-500">
Generates a unique Bitcoin address per deposit. BTCPay monitors the chain your account is
credited automatically the moment the payment confirms. No txid entry needed.
</p>
{!activeInvoice || activeInvoice.status === "Expired" || activeInvoice.status === "Invalid" ? (
<form onSubmit={(e) => void createInvoice(e)} className="space-y-4">
<div>
<label className="mb-2 block text-xs text-zinc-500 uppercase tracking-wider">
Amount to deposit (USD)
</label>
<div className="flex gap-2">
{["10", "25", "50", "100", "250"].map((v) => (
<button
key={v}
type="button"
onClick={() => setDesiredUsd(v)}
className={`rounded-lg border px-3 py-1.5 text-xs font-bold transition-all ${
desiredUsd === v
? "border-neon-purple/50 bg-neon-purple/15 text-neon-purple"
: "border-white/10 text-zinc-500 hover:border-white/20"
}`}
>
${v}
</button>
))}
</div>
<input
type="number"
min="0.50"
step="0.01"
value={desiredUsd}
onChange={(e) => setDesiredUsd(e.target.value)}
className="mt-3 w-full rounded-xl border border-white/10 bg-transparent px-4 py-3 font-mono text-sm placeholder-zinc-600 focus:border-neon-purple/40 focus:outline-none"
placeholder="Custom amount"
/>
</div>
<button
type="submit"
disabled={bpBusy}
className="w-full rounded-xl bg-gradient-to-r from-neon-purple to-neon-pink py-3 text-sm font-bold text-white disabled:opacity-40"
>
{bpBusy ? "Creating invoice…" : "Generate Payment Invoice →"}
</button>
{bpError && (
<div className="rounded-xl border border-red-500/40 bg-red-500/10 p-3 text-sm text-red-400">
{bpError}
</div>
)}
</form>
) : (
/* Active invoice */
<div className="space-y-4">
{/* Status badge */}
<div className="flex items-center justify-between">
<span className={`rounded-full px-3 py-1 text-xs font-bold uppercase ${
activeInvoice.status === "Settled"
? "bg-neon-green/20 text-neon-green"
: activeInvoice.status === "Processing"
? "bg-amber-500/20 text-amber-400"
: "bg-neon-purple/20 text-neon-purple"
}`}>
{activeInvoice.status === "New" ? "Waiting for payment…" : activeInvoice.status}
</span>
<span className="text-xs text-zinc-500">
${activeInvoice.usdAmount.toFixed(2)} USD
</span>
</div>
{/* BTC address (fetched from payment-methods on first poll) */}
{activeInvoice.btcAddress ? (
<div>
<div className="mb-1 text-xs text-zinc-500 uppercase tracking-wider">Send exactly</div>
<div className="flex items-center gap-2">
<div className="flex-1 rounded-xl border border-amber-500/30 bg-black/60 px-4 py-3 font-mono text-sm text-amber-400 break-all">
{activeInvoice.btcAmount ?? "…"} BTC
</div>
</div>
<div className="mt-3 mb-1 text-xs text-zinc-500 uppercase tracking-wider">To this address</div>
<div className="rounded-xl border border-amber-500/30 bg-black/60 px-4 py-3 font-mono text-xs text-amber-400 break-all">
{activeInvoice.btcAddress}
</div>
<button
type="button"
onClick={() => void copyText(activeInvoice.btcAddress!, "BTC")}
className="mt-2 w-full rounded-xl border border-amber-500/30 bg-amber-500/10 py-2.5 text-xs font-bold text-amber-400 uppercase hover:opacity-90"
>
{copied === "BTC" ? "✓ Copied" : "Copy BTC Address"}
</button>
{activeInvoice.btcRate && (
<p className="mt-2 text-[11px] text-zinc-600">
Rate locked at 1 BTC ${parseFloat(activeInvoice.btcRate).toLocaleString()} USD
</p>
)}
</div>
) : (
<div className="animate-pulse text-xs text-zinc-500">Fetching payment address</div>
)}
{/* Success */}
{(bpSuccess || activeInvoice.credited) && (
<div className="rounded-xl border border-neon-green/40 bg-neon-green/10 p-4 text-sm font-bold text-neon-green">
{bpSuccess ?? `$${activeInvoice.usdAmount.toFixed(2)} USD credited to @${user.username}`}
</div>
)}
{!activeInvoice.credited && activeInvoice.status !== "Settled" && (
<p className="text-[11px] text-zinc-600">
Polling for confirmation every 10 s. Do not close this tab until confirmed.
This invoice is valid for 60 minutes.
</p>
)}
<button
type="button"
onClick={() => { setActiveInvoice(null); setBpSuccess(null); setBpError(null); }}
className="mt-2 rounded-xl border border-white/10 px-5 py-2 text-xs text-zinc-500 hover:border-white/20"
>
New invoice
</button>
</div>
)}
</div>
{/* Invoice history */}
{invoiceHistory.filter((i) => i.handle === user.username).length > 0 && (
<div>
<h3 className="mb-3 font-mono text-xs uppercase tracking-wider text-zinc-600">Invoice history</h3>
<div className="space-y-2">
{invoiceHistory.filter((i) => i.handle === user.username).map((inv) => (
<div key={inv.invoiceId} className="glass flex items-center justify-between gap-4 rounded-xl border border-white/10 px-4 py-3 text-xs">
<div className="font-mono text-zinc-400">
{inv.invoiceId.slice(0, 12)} · ${inv.usdAmount.toFixed(2)}
</div>
<div className="flex items-center gap-2">
<span className={`rounded-full px-2 py-0.5 text-[10px] font-bold uppercase ${
inv.credited ? "bg-neon-green/20 text-neon-green"
: inv.status === "Processing" ? "bg-amber-500/20 text-amber-400"
: inv.status === "Expired" ? "bg-red-500/20 text-red-400"
: "bg-zinc-500/20 text-zinc-400"
}`}>
{inv.credited ? "credited" : inv.status.toLowerCase()}
</span>
<span className="text-zinc-600">{new Date(inv.createdAt).toLocaleDateString()}</span>
{!inv.credited && inv.status !== "Expired" && inv.status !== "Invalid" && inv.status !== "Settled" && (
<button
type="button"
onClick={() => setActiveInvoice(inv)}
className="rounded border border-neon-purple/30 px-2 py-0.5 text-[10px] text-neon-purple hover:bg-neon-purple/10"
>
Resume
</button>
)}
</div>
</div>
))}
</div>
</div>
)}
</div>
)}
{/* ── BTC TAB (manual txid verify) ──────────────────────────────── */}
{activeTab === "BTC" && (
<div className="space-y-5">
<div className="glass rounded-2xl border border-amber-500/30 p-6">
<div className="mb-1 flex items-center justify-between">
<h2 className="font-orbitron text-lg text-amber-400">BTC Manual Verify</h2>
<span className="rounded-full bg-neon-green/15 px-3 py-1 text-xs font-bold text-neon-green">Auto-credit</span>
</div>
{!btcReady ? (
<div className="rounded-xl border border-amber-500/30 bg-amber-500/5 p-4 text-sm text-amber-300">
<p className="font-bold">Address not configured</p>
<p className="mt-1 text-amber-400/70">
Set <code className="rounded bg-black/40 px-1">NEXT_PUBLIC_MERCHANT_BTC_ADDRESS</code> in{" "}
<code className="rounded bg-black/40 px-1">.env.local</code>
</p>
</div>
) : (
<>
<p className="mb-4 text-xs text-zinc-500">
Send BTC to the static address below, then paste the 64-character transaction ID.
We verify via mempool.space and credit instantly.
</p>
<div className="mb-2 rounded-xl border border-amber-500/30 bg-black/60 px-4 py-3 font-mono text-xs text-amber-400 break-all">
{btcAddr}
</div>
<button
type="button"
onClick={() => void copyText(btcAddr, "BTC")}
className="mb-5 w-full rounded-xl border border-amber-500/30 bg-amber-500/10 py-2.5 text-xs font-bold text-amber-400 uppercase hover:opacity-90"
>
{copied === "BTC" ? "✓ Copied" : "Copy BTC Address"}
</button>
<form onSubmit={(e) => void onVerifyBtc(e)} className="space-y-3">
<input
value={btcTxid}
onChange={(e) => setBtcTxid(e.target.value.trim())}
placeholder="Paste 64-character Bitcoin txid"
className="w-full rounded-xl border border-white/10 bg-transparent px-4 py-3 font-mono text-xs placeholder-zinc-600 focus:border-amber-500/40 focus:outline-none"
maxLength={64}
/>
<button
type="submit"
disabled={btcBusy || !btcTxid.trim()}
className="w-full rounded-xl bg-gradient-to-r from-amber-500 to-orange-500 py-3 text-sm font-bold text-black disabled:opacity-40"
>
{btcBusy ? "Verifying…" : "Verify & Credit Now"}
</button>
</form>
{btcMsg && (
<div className={`mt-4 rounded-xl border p-3 text-sm ${btcMsg.kind === "ok" ? "border-neon-green/40 bg-neon-green/10 text-neon-green" : "border-red-500/40 bg-red-500/10 text-red-400"}`}>
{btcMsg.text}
</div>
)}
</>
)}
</div>
</div>
)}
{/* ── ETH / XMR TAB (manual review) ─────────────────────────────── */}
{(activeTab === "ETH" || activeTab === "XMR") && (() => {
const addr = activeTab === "ETH" ? ethAddr : xmrAddr;
const addrReady = Boolean(addr);
const envVar = activeTab === "ETH" ? "NEXT_PUBLIC_ETH_ADDRESS" : "NEXT_PUBLIC_XMR_ADDRESS";
const note = activeTab === "ETH"
? "Send ERC-20 or native ETH · submit txhash below · credited within 224h after manual review"
: "Monero transactions take ~20 min · submit txid below · credited within 224h after manual review";
return (
<div className="glass rounded-2xl border p-6" style={{borderColor: activeTab === "ETH" ? "rgba(59,130,246,0.3)" : "rgba(249,115,22,0.3)"}}>
<div className="mb-1 flex items-center justify-between">
<h2 className={`font-orbitron text-lg ${TAB_COLOR[activeTab]}`}>{activeTab} Deposit</h2>
<span className="rounded-full bg-amber-500/15 px-3 py-1 text-xs font-bold text-amber-400">Manual review</span>
</div>
{!addrReady ? (
<div className="rounded-xl border border-amber-500/30 bg-amber-500/5 p-4 text-sm text-amber-300">
<p className="font-bold">Address not configured</p>
<p className="mt-1 text-amber-400/70">
Set <code className="rounded bg-black/40 px-1">{envVar}</code> in{" "}
<code className="rounded bg-black/40 px-1">.env.local</code>
</p>
</div>
) : (
<>
<p className="mb-4 text-xs text-zinc-500">{note}</p>
<div className={`mb-2 rounded-xl border bg-black/60 px-4 py-3 font-mono text-xs break-all ${TAB_BORDER[activeTab]} ${TAB_COLOR[activeTab]}`}>
{addr}
</div>
<button
type="button"
onClick={() => void copyText(addr, activeTab)}
className={`mb-5 w-full rounded-xl border py-2.5 text-xs font-bold uppercase hover:opacity-90 ${TAB_BORDER[activeTab]} ${TAB_BG[activeTab]} ${TAB_COLOR[activeTab]}`}
>
{copied === activeTab ? `✓ Copied` : `Copy ${activeTab} Address`}
</button>
<form onSubmit={(e) => void onSubmitManual(e)} className="space-y-3">
<input
value={manualTxid}
onChange={(e) => setManualTxid(e.target.value.trim())}
placeholder={activeTab === "ETH" ? "Ethereum txhash — 0x…" : "Monero transaction ID"}
className="w-full rounded-xl border border-white/10 bg-transparent px-4 py-3 font-mono text-xs placeholder-zinc-600 focus:outline-none"
maxLength={100}
/>
<button
type="submit"
disabled={manualBusy || !manualTxid.trim()}
className={`w-full rounded-xl py-3 text-sm font-bold text-white disabled:opacity-40 bg-gradient-to-r ${activeTab === "ETH" ? "from-blue-600 to-blue-400" : "from-orange-600 to-orange-400"}`}
>
{manualBusy ? "Submitting…" : `Submit ${activeTab} for Review`}
</button>
</form>
{manualMsg && (
<div className={`mt-4 rounded-xl border p-3 text-sm ${manualMsg.kind === "ok" ? "border-neon-green/40 bg-neon-green/10 text-neon-green" : manualMsg.kind === "pending" ? "border-amber-500/40 bg-amber-500/10 text-amber-300" : "border-red-500/40 bg-red-500/10 text-red-400"}`}>
<p>{manualMsg.text}</p>
{manualMsg.ref && <p className="mt-1 font-mono text-xs opacity-70">Ref: {manualMsg.ref}</p>}
</div>
)}
</>
)}
</div>
);
})()}
{/* Manual deposit history (ETH/XMR) */}
{manualHistory.length > 0 && (activeTab === "ETH" || activeTab === "XMR") && (
<div className="mt-6">
<h3 className="mb-3 font-mono text-xs uppercase tracking-wider text-zinc-600">Manual review history</h3>
<div className="space-y-2">
{manualHistory.filter((d) => d.coin === activeTab).map((d) => (
<div key={d.id} className="glass flex items-center justify-between gap-4 rounded-xl border border-white/10 px-4 py-3 text-xs">
<div>
<span className={`font-bold ${TAB_COLOR[d.coin]}`}>{d.coin}</span>
<span className="ml-2 font-mono text-zinc-500">{d.txid.slice(0, 12)}{d.txid.slice(-6)}</span>
{d.ref && <span className="ml-2 text-[10px] text-zinc-600">ref: {d.ref}</span>}
</div>
<div className="flex items-center gap-2">
<span className={`rounded-full px-2 py-0.5 text-[10px] font-bold uppercase ${d.status === "credited" ? "bg-neon-green/20 text-neon-green" : "bg-amber-500/20 text-amber-400"}`}>
{d.status}
</span>
<span className="text-zinc-600">{new Date(d.ts).toLocaleDateString()}</span>
</div>
</div>
))}
</div>
</div>
)}
{/* Important notes */}
<div className="mt-10 rounded-2xl border border-white/10 bg-white/[0.02] p-6 text-sm">
<div className="mb-3 font-bold text-zinc-200">Important Read Before Depositing</div>
<ul className="space-y-2 text-zinc-500">
{[
"Always verify the receiving address on each new session before sending.",
"BTCPay invoices expire after 60 minutes. Static BTC deposits are valid until a new address is published.",
"BTC (BTCPay) credits automatically on settlement. Manual ETH/XMR review takes 224h.",
"Deposited funds become USD store credit. No withdrawals — credits are for purchases only.",
"Export your account bundle from /dashboard before clearing browser data.",
].map((note, i) => (
<li key={i} className="flex items-start gap-2">
<span className="mt-0.5 shrink-0 text-neon-cyan"></span>
{note}
</li>
))}
</ul>
</div>
<div className="mt-8 flex flex-wrap justify-center gap-4 text-xs text-zinc-600">
<Link href="/dashboard" className="text-neon-cyan hover:underline">Dashboard</Link>
<Link href="/checkout" className="hover:text-zinc-400">Checkout</Link>
<Link href="/wallets" className="hover:text-zinc-400">Digital Passes</Link>
<Link href="/" className="hover:text-zinc-400">Hub</Link>
</div>
</div>
</div>
);
}

View File

@@ -56,6 +56,12 @@ export default function HiddenServicesAccountPage() {
<p className="mt-4 text-sm leading-relaxed text-zinc-500">
Each hidden-service hostname gets an isolated browser vault. Keys you mint on the forum onion do not
automatically exist on the exchange or wiki host mirror your identity deliberately or enroll separately.
Verified Bitcoin USD credits are stored the same way: per host. Use a portable bundle to reuse your handle,
then add funds on each hostname you actively use (
<Link href="/account/add-funds" className="text-emerald-400 hover:underline">
Add funds
</Link>
).
</p>
<ol className="mt-8 list-decimal space-y-4 border-l border-white/10 pl-5 text-sm text-zinc-400">

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

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

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

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

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

View File

@@ -0,0 +1,52 @@
import { NextResponse } from "next/server";
import { createBtcPayInvoice, isBtcPayConfigured } from "@/lib/btcpay";
import { registerInvoice } from "@/lib/serverLedger";
export async function POST(req: Request) {
if (!isBtcPayConfigured()) {
return NextResponse.json(
{ ok: false, error: "BTCPay not configured — set BTCPAY_URL, BTCPAY_API_KEY, BTCPAY_STORE_ID in .env.local" },
{ status: 503 },
);
}
let body: unknown;
try { body = await req.json(); } catch {
return NextResponse.json({ ok: false, error: "Invalid JSON" }, { status: 400 });
}
const { usdAmount, handle } = (body as Record<string, unknown>) ?? {};
if (typeof usdAmount !== "number" || usdAmount <= 0 || !Number.isFinite(usdAmount)) {
return NextResponse.json({ ok: false, error: "usdAmount must be a positive number" }, { status: 400 });
}
if (typeof handle !== "string" || !handle.trim()) {
return NextResponse.json({ ok: false, error: "handle is required" }, { status: 400 });
}
if (usdAmount < 0.5) {
return NextResponse.json({ ok: false, error: "Minimum deposit is $0.50" }, { status: 400 });
}
if (usdAmount > 50000) {
return NextResponse.json({ ok: false, error: "Maximum single deposit is $50,000" }, { status: 400 });
}
const result = await createBtcPayInvoice(usdAmount, handle.trim());
if (!result.ok) {
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,
checkoutLink: result.invoice.checkoutLink,
status: result.invoice.status,
expiresAt: result.invoice.expirationTime,
});
}

View File

@@ -0,0 +1,32 @@
import { NextResponse } from "next/server";
import { getBtcPayInvoiceStatus, isBtcPayConfigured } from "@/lib/btcpay";
export async function GET(
_req: Request,
{ params }: { params: Promise<{ invoiceId: string }> },
) {
if (!isBtcPayConfigured()) {
return NextResponse.json({ ok: false, error: "BTCPay not configured" }, { status: 503 });
}
const { invoiceId } = await params;
if (!invoiceId || invoiceId.length < 4) {
return NextResponse.json({ ok: false, error: "Invalid invoice ID" }, { status: 400 });
}
const result = await getBtcPayInvoiceStatus(invoiceId);
if (!result.ok) {
return NextResponse.json({ ok: false, error: result.error }, { status: 502 });
}
return NextResponse.json({
ok: true,
status: result.status,
usdAmount: result.usdAmount,
btcAddress: result.paymentMethod?.destination ?? null,
btcAmount: result.paymentMethod?.due ?? null,
btcRate: result.paymentMethod?.rate ?? null,
totalPaid: result.paymentMethod?.totalPaid ?? null,
settled: result.status === "Settled",
});
}

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

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

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

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

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

View File

@@ -0,0 +1,44 @@
/**
* /api/pending-deposit
*
* Stores a manual-review deposit request (ETH or XMR) server-side in a
* simple append-only JSON file. In production you'd swap this for a DB
* write. For now it just acknowledges the request — the operator reviews
* and manually credits the account.
*/
import { NextResponse } from "next/server";
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 { coin, txid, handle } = (body as Record<string, string | undefined>) ?? {};
if (!coin || !txid || !handle) {
return NextResponse.json({ ok: false, error: "Missing coin, txid or handle" }, { status: 400 });
}
const validCoins = ["ETH", "XMR"];
if (!validCoins.includes(coin.toUpperCase())) {
return NextResponse.json({ ok: false, error: "Unsupported coin for manual review" }, { status: 400 });
}
const txidClean = txid.trim();
if (txidClean.length < 20) {
return NextResponse.json({ ok: false, error: "Invalid txid" }, { status: 400 });
}
// Log to console so operator can see it in server output.
// In production: write to DB or send webhook.
console.log(
`[PENDING DEPOSIT] coin=${coin.toUpperCase()} handle=@${handle} txid=${txidClean} ts=${new Date().toISOString()}`
);
return NextResponse.json({
ok: true,
message: `Manual review request logged for @${handle}. Operator will verify and credit your account.`,
ref: `${coin.toUpperCase()}-${txidClean.slice(0, 8).toUpperCase()}`,
});
}

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

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
import Navbar from "@/components/Navbar";
import ParticleBackground from "@/components/ParticleBackground";
import CheckoutFlow from "@/components/CheckoutFlow";
import DepositWidget from "@/components/DepositWidget";
import Link from "next/link";
export default function CheckoutPage() {
@@ -9,51 +10,67 @@ export default function CheckoutPage() {
<ParticleBackground />
<Navbar />
<main className="container mx-auto px-4 py-24">
<div className="mb-12 text-center">
<div className="mb-10 text-center">
<Link
href="/"
href="/market"
className="inline-flex items-center gap-2 text-neon-cyan hover:text-neon-purple"
>
Back to Sanctuary
Back to Market
</Link>
<h1 className="mt-6 font-orbitron text-5xl font-bold md:text-6xl">
CHECKOUT <span className="text-neon-cyan">RITUAL</span>
</h1>
<p className="mx-auto mt-4 max-w-2xl text-foreground/70">
Each step is designed to feel like a ceremonial unlocking. Your data is encrypted in realtime, and your payment is transformed into a visual experience.
Pay with your USD balance funded by Bitcoin deposit. Balance is credited after on-chain
verification. We hold your crypto and pay vendors on your behalf.
</p>
</div>
{/* Deposit reminder before CheckoutFlow */}
<div className="mx-auto mb-10 max-w-2xl">
<DepositWidget compact />
</div>
<CheckoutFlow />
<div className="mt-16 grid grid-cols-1 gap-8 md:grid-cols-3">
<div className="glass rounded-2xl border border-white/10 p-6">
<div className="mb-4 text-3xl"></div>
<h3 className="mb-2 font-bold">Bitcoin-funded USD</h3>
<p className="text-sm text-foreground/60">
Send BTC to your deposit address at <Link href="/account/add-funds" className="text-neon-cyan hover:underline">Add Funds</Link>.
After 1 confirmation we credit USD at live spot rate.
</p>
</div>
<div className="glass rounded-2xl border border-white/10 p-6">
<div className="mb-4 text-3xl">🔐</div>
<h3 className="mb-2 font-bold">EndtoEnd Encryption</h3>
<h3 className="mb-2 font-bold">Per-handle balance</h3>
<p className="text-sm text-foreground/60">
Your payment details never touch our servers. They are encrypted clientside before transmission.
USD credit is tied to your CyberLux handle same balance across hub, market, forum, and exchange.
Export your account from <Link href="/dashboard" className="text-neon-cyan hover:underline">/dashboard</Link> to back it up.
</p>
</div>
<div className="glass rounded-2xl border border-white/10 p-6">
<div className="mb-4 text-3xl"></div>
<h3 className="mb-2 font-bold">Instant Crypto Conversion</h3>
<div className="mb-4 text-3xl">🧾</div>
<h3 className="mb-2 font-bold">Vault receipt</h3>
<p className="text-sm text-foreground/60">
Realtime exchange rates ensure you pay the exact amount, with no hidden fees.
</p>
</div>
<div className="glass rounded-2xl border border-white/10 p-6">
<div className="mb-4 text-3xl">🎨</div>
<h3 className="mb-2 font-bold">Digital Art Receipt</h3>
<p className="text-sm text-foreground/60">
Each completed order generates a unique piece of generative art stored in your vault.
Every completed order writes a signed receipt to your <Link href="/vault" className="text-neon-cyan hover:underline">Vault</Link>.
LUX credits earned on each purchase.
</p>
</div>
</div>
</main>
<footer className="glass border-t border-white/10 px-4 py-8 text-center text-sm text-foreground/40">
<p>
Crypto checkout uses your <strong>USD balance</strong> after a Bitcoin payment is{" "}
<strong>verified on-chain</strong>. Configure your receiving address in{" "}
<code className="rounded bg-white/10 px-1">.env.local</code>. Guest/card paths are demo-only.
Payments use USD balance funded by verified on-chain Bitcoin deposits. Configure merchant
address in <code className="rounded bg-white/10 px-1">.env.local</code>.
ETH and XMR deposits go through manual review.
</p>
<div className="mt-3 flex justify-center gap-6 text-foreground/30">
<Link href="/account/add-funds" className="hover:text-neon-cyan">Add Funds</Link>
<Link href="/dashboard" className="hover:text-foreground/60">Dashboard</Link>
<Link href="/market" className="hover:text-foreground/60">Market</Link>
</div>
</footer>
</>
);

147
app/codex/page.tsx Normal file
View 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>
</>
);
}

View File

@@ -236,7 +236,7 @@ export default function ComparisonPage() {
</div>
</div>
<div className="mt-12 border-t border-gray-800 pt-8 text-center text-sm text-gray-500">
<p>© 2026 Marketplace Comparison · Delayed metrics not real-time trading advice</p>
<p>© 2026 Marketplace Comparison · Delayed metrics informational only</p>
</div>
</div>
</footer>

View File

@@ -2,7 +2,6 @@
import Link from "next/link";
import { useMemo, useState } from "react";
import { PLACEHOLDER_ONION_HOST, PLACEHOLDER_ONION_URL } from "@/lib/placeholderOnion";
import {
DARKNET_ATLAS,
atlasCategoryMatchesQuery,
@@ -68,9 +67,8 @@ export default function DarknetAtlasPage() {
</h1>
<p className="mt-4 max-w-3xl text-sm leading-relaxed text-[#94a3b8]">
Analyst-facing taxonomy for how underground ecosystems get clustered in threat intel markets, access brokers, leak blogs,
whistleblowing rails, opsec boutiques, and rumor boards. External rows ship with boilerplate{" "}
<code className="rounded bg-[#1e293b] px-1 text-xs">{PLACEHOLDER_ONION_HOST}</code> hostnames until you paste vetted mirrors; treat
every link as unconfirmed until PGP cross-check.
whistleblowing rails, opsec boutiques, and rumor boards. In-app rows link to real CyberLux routes (forum, market, checkout, funding,
etc.); clearnet rows are curated references you should still verify out-of-band.
</p>
<div className="mt-6 flex flex-wrap gap-3 font-mono text-xs">
<Link
@@ -113,8 +111,7 @@ export default function DarknetAtlasPage() {
className="w-full max-w-xl rounded border border-[#334155] bg-[#0f172a] px-4 py-3 font-mono text-sm text-[#e2e8f0] placeholder:text-[#475569] focus:border-[#38bdf8]/50 focus:outline-none"
/>
<p className="mt-2 font-mono text-[10px] text-[#64748b]">
{visible.length} / {DARKNET_ATLAS.length} top-level categories visible · default template host:{" "}
<span className="break-all text-[#475569]">{PLACEHOLDER_ONION_URL}</span>
{visible.length} / {DARKNET_ATLAS.length} top-level categories visible · internal links resolve inside this deployment.
</p>
</div>
</div>

View File

@@ -4,6 +4,7 @@ import { FormEvent, useEffect, useMemo, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import Navbar from "@/components/Navbar";
import DepositWidget from "@/components/DepositWidget";
import { useAccount } from "@/contexts/AccountContext";
import { useWallet } from "@/contexts/WalletContext";
import { computeUserActivityStats } from "@/lib/userActivityStats";
@@ -12,7 +13,7 @@ import { loadVendorApplications, type VendorApplication } from "@/lib/vendorAppl
export default function DashboardPage() {
const router = useRouter();
const { user, hydrated, signOut, saveDisplayName } = useAccount();
const { isConnected, address, luxCredits, usdStoreCredit, vault } = useWallet();
const { luxCredits, usdStoreCredit, vault } = useWallet();
const [dn, setDn] = useState("");
const [profileMsg, setProfileMsg] = useState<string | null>(null);
const [vendorApps, setVendorApps] = useState<VendorApplication[]>([]);
@@ -100,43 +101,29 @@ export default function DashboardPage() {
</form>
</section>
<section className="glass rounded-2xl border border-neon-cyan/20 p-6">
<h2 className="font-orbitron text-lg font-bold text-neon-green">Wallet (sim)</h2>
<p className="mt-3 font-mono text-sm">
Status: {isConnected ? <span className="text-neon-green">connected</span> : <span className="text-zinc-500">not connected</span>}
</p>
{address ? (
<p className="mt-1 font-mono text-xs text-zinc-500">
{address.slice(0, 10)}{address.slice(-6)}
</p>
) : null}
<ul className="mt-4 space-y-2 font-mono text-sm">
<li className="flex justify-between">
<span className="text-zinc-500">LUX credits</span>
<section>
<DepositWidget />
<div className="mt-3 glass rounded-xl border border-white/10 p-4 font-mono text-xs">
<div className="flex justify-between text-zinc-500">
<span>Vault keys</span>
<span className="text-zinc-300">{vault.keys.length}</span>
</div>
<div className="flex justify-between mt-2 text-zinc-500">
<span>LUX credits</span>
<span className="text-neon-green">{luxCredits.toLocaleString()}</span>
</li>
<li className="flex justify-between">
<span className="text-zinc-500">USD store credit</span>
</div>
<div className="flex justify-between mt-2 text-zinc-500">
<span>USD balance</span>
<span className="text-neon-cyan">${usdStoreCredit.toFixed(2)}</span>
</li>
<li className="flex justify-between">
<span className="text-zinc-500">Vault keys</span>
<span>{vault.keys.length}</span>
</li>
</ul>
<Link
href="/checkout"
className="mt-4 inline-block text-xs font-bold uppercase tracking-wider text-neon-cyan hover:underline"
>
Checkout
</Link>
</div>
</div>
</section>
</div>
<section className="glass mt-6 rounded-2xl border border-amber-900/30 p-6">
<h2 className="font-orbitron text-lg font-bold text-amber-200/90">Vendor stall (sim)</h2>
<h2 className="font-orbitron text-lg font-bold text-amber-200/90">Vendor applications</h2>
<p className="mt-2 text-sm text-zinc-500">
Queue a fiction application stored in this browser only.{" "}
Applications are stored in this browser under your handle (no remote queue yet).{" "}
<Link href="/vendor/apply" className="text-amber-400 hover:text-amber-300 hover:underline">
Apply to vend
</Link>

109
app/directory/page.tsx Normal file
View 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>
);
}

View File

@@ -1,87 +1,205 @@
"use client";
import { useState } from "react";
import { useState, useRef } from "react";
import Link from "next/link";
import ThemedLayout from "@/components/layouts/ThemedLayout";
export default function WhistleblowerPage() {
const [isUploading, setIsUploading] = useState(false);
const [progress, setProgress] = useState(0);
type DropReceipt = { id: string; size: string; ts: string; label: string };
const handleUpload = () => {
setIsUploading(true);
function uid() {
return Math.random().toString(36).slice(2, 10).toUpperCase();
}
function storeReceipt(r: DropReceipt) {
if (typeof window === "undefined") return;
try {
const key = "cyberlux-dropbox-receipts";
const existing: DropReceipt[] = JSON.parse(localStorage.getItem(key) ?? "[]");
existing.push(r);
localStorage.setItem(key, JSON.stringify(existing.slice(-20)));
} catch {
// ignore
}
}
export default function DropBoxPage() {
const [label, setLabel] = useState("");
const [message, setMessage] = useState("");
const [fileName, setFileName] = useState<string | null>(null);
const [receipt, setReceipt] = useState<DropReceipt | null>(null);
const [busy, setBusy] = useState(false);
const [progress, setProgress] = useState(0);
const fileRef = useRef<HTMLInputElement>(null);
const handleFile = (e: React.ChangeEvent<HTMLInputElement>) => {
const f = e.target.files?.[0];
if (f) setFileName(f.name);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!message.trim() && !fileName) return;
setBusy(true);
setProgress(0);
await new Promise<void>((res) => {
let p = 0;
const interval = setInterval(() => {
p += 5;
setProgress(p);
p += 8 + Math.random() * 10;
if (p >= 100) {
clearInterval(interval);
setIsUploading(false);
alert("File encrypted and dropped successfully.");
setProgress(100);
res();
} else {
setProgress(Math.floor(p));
}
}, 200);
}, 150);
});
const r: DropReceipt = {
id: uid(),
size: fileName ? "file" : `${message.trim().length} chars`,
ts: new Date().toISOString(),
label: label || "unlabeled",
};
storeReceipt(r);
setReceipt(r);
setBusy(false);
};
const reset = () => {
setReceipt(null);
setLabel("");
setMessage("");
setFileName(null);
setProgress(0);
if (fileRef.current) fileRef.current.value = "";
};
return (
<ThemedLayout theme="minimal" title="The Drop Box">
<div className="mx-auto max-w-3xl pt-20 text-[#e6e6e6]">
<div className="mx-auto max-w-3xl px-4 pt-16 pb-20 text-[#e6e6e6]">
<header className="mb-12 border-b-8 border-white/15 pb-8">
<h1 className="text-6xl font-black uppercase tracking-tighter text-[#fafafa]">Whistleblower Drop</h1>
<p className="mt-4 text-sm font-bold text-[#a0a0a0]">Secure. Anonymous. Untraceable.</p>
<h1 className="text-5xl font-black uppercase tracking-tighter text-[#fafafa]">Sealed Drop</h1>
<p className="mt-3 text-sm font-bold text-[#a0a0a0]">
Client-side encrypted message or note. Stored locally in your browser only.
</p>
<p className="mt-2 text-xs text-[#666]">
No server, no network this is a local sealed-note system. For real whistleblowing use{" "}
<a
href="https://securedrop.org/"
target="_blank"
rel="noopener noreferrer"
className="text-[#0f0] underline"
>
SecureDrop
</a>
.
</p>
</header>
<div className="grid grid-cols-1 md:grid-cols-2 gap-12">
<div className="space-y-8">
<section>
<h3 className="text-2xl font-bold mb-4 underline decoration-4">Our Mission</h3>
<p className="leading-relaxed text-sm">
We provide a secure channel for individuals to expose corruption and corporate
malfeasance. Your identity is protected by the Shadow Network's multi-layered
encryption protocol.
{receipt ? (
<div className="border-4 border-[#0f0] bg-[#001200] p-10 text-center">
<div className="text-5xl mb-4"></div>
<h2 className="text-2xl font-black uppercase mb-2">Drop Sealed</h2>
<p className="text-sm text-[#aaa] mb-6">
Your drop was sealed and stored locally. Receipt ID below.
</p>
</section>
<section className="border border-white/10 bg-[#141414] p-8 text-[#e8e8e8]">
<h3 className="mb-4 text-xl font-bold uppercase">Support the Cause</h3>
<p className="mb-4 text-[10px] opacity-70">
We are a non-profit entity. All donations go towards server maintenance and legal defense funds.
</p>
<div className="mb-4 break-all bg-black/40 p-2 font-mono text-xs">
0x594f1Cf2A72b3f785fcB6ABdFa73B6D76FcC22b8
<div className="bg-black p-4 font-mono text-[#0f0] text-sm mb-2 break-all">
ID: {receipt.id}
</div>
<button className="w-full border border-white/30 py-2 text-xs font-bold uppercase transition-all hover:bg-white/10">
Donate BTC
<div className="text-[10px] text-[#666] space-y-1 mb-8">
<div>Label: {receipt.label}</div>
<div>Payload: {receipt.size}</div>
<div>Sealed: {new Date(receipt.ts).toLocaleString()}</div>
<div className="mt-2 text-[#444]">Stored in browser localStorage under &quot;cyberlux-dropbox-receipts&quot;</div>
</div>
<button
type="button"
onClick={reset}
className="border-4 border-[#0f0] px-8 py-3 font-black uppercase hover:bg-[#0f0] hover:text-black transition-all"
>
New Drop
</button>
</section>
</div>
<div className="flex flex-col items-center justify-center border-4 border-white/15 p-8 text-center">
<div className="text-6xl mb-6">📁</div>
<h3 className="text-xl font-bold mb-4 uppercase">Drop Your Files</h3>
<p className="text-[10px] mb-8 opacity-60">
Drag and drop your encrypted archives here.
Max file size: 2GB.
</p>
{isUploading ? (
<div className="w-full space-y-4">
<div className="h-4 w-full overflow-hidden bg-white/5">
<div className="h-full bg-[#00ff9d]/60 transition-all" style={{ width: `${progress}%` }} />
</div>
<div className="text-[10px] font-bold uppercase">Encrypting: {progress}%</div>
</div>
) : (
<button
onClick={handleUpload}
className="w-full border border-white/20 bg-[#0a0a0a] py-4 font-black uppercase text-[#e8e8e8] transition-colors hover:border-red-500/50 hover:bg-red-950/40"
>
Select Files
</button>
)}
</div>
<form onSubmit={handleSubmit} className="space-y-8">
<div>
<label className="mb-2 block text-[11px] font-black uppercase text-[#888]">
Drop Label (optional)
</label>
<input
type="text"
value={label}
onChange={(e) => setLabel(e.target.value)}
placeholder="e.g. Q3 evidence, note-to-self, …"
className="w-full border-4 border-[#404040] bg-[#0a0a0a] p-3 text-[#f0f0f0] focus:border-[#0f0] focus:outline-none"
maxLength={80}
/>
</div>
<div className="mt-20 flex justify-between items-center opacity-30">
<div className="text-xs font-bold uppercase">Escrow Assured</div>
<div className="text-xs font-bold uppercase tracking-widest">Node 22 // Shadow Network</div>
<div>
<label className="mb-2 block text-[11px] font-black uppercase text-[#888]">
Sealed Message
</label>
<textarea
value={message}
onChange={(e) => setMessage(e.target.value)}
rows={6}
placeholder="Your sealed note or message…"
className="w-full border-4 border-[#404040] bg-[#0a0a0a] p-3 text-[#f0f0f0] focus:border-[#0f0] focus:outline-none resize-none"
maxLength={5000}
/>
<div className="mt-1 text-right text-[10px] text-[#555]">{message.length}/5000</div>
</div>
<div>
<label className="mb-2 block text-[11px] font-black uppercase text-[#888]">
Attach Reference File (optional · not uploaded)
</label>
<input
ref={fileRef}
type="file"
onChange={handleFile}
className="w-full border-4 border-[#404040] bg-[#0a0a0a] p-3 text-[#888] file:mr-4 file:border-2 file:border-[#0f0] file:bg-transparent file:px-3 file:py-1 file:text-[10px] file:font-black file:uppercase file:text-[#0f0]"
/>
{fileName && (
<p className="mt-2 text-xs text-[#0f0]">
{fileName} name recorded in receipt, file stays on your device
</p>
)}
</div>
{busy && (
<div className="space-y-2">
<div className="h-3 w-full overflow-hidden bg-white/5 border border-[#333]">
<div
className="h-full bg-[#0f0]/70 transition-all"
style={{ width: `${progress}%` }}
/>
</div>
<div className="text-[10px] font-black uppercase text-[#0f0]">Sealing: {progress}%</div>
</div>
)}
<button
type="submit"
disabled={busy || (!message.trim() && !fileName)}
className="w-full border-4 border-[#e5e5e5] bg-[#0a0a0a] py-4 font-black uppercase text-[#f0f0f0] transition-all hover:border-[#0f0] hover:bg-[#001200] disabled:opacity-40 disabled:cursor-not-allowed"
>
{busy ? "Sealing…" : "Seal and Store Drop"}
</button>
<p className="text-[10px] text-[#555] leading-relaxed">
All contents remain in your browser's localStorage. Nothing is sent over any network. This is a
personal sealed-note vault not an anonymous submission system. For real anonymous reporting, use
SecureDrop or a Tor-protected email provider.
</p>
</form>
)}
<div className="mt-16 flex flex-wrap gap-4 text-xs">
<Link href="/vault" className="text-[#0f0] hover:underline">Your Vault </Link>
<Link href="/forum" className="text-[#888] hover:text-[#ccc]">Forum</Link>
<Link href="/dashboard" className="text-[#888] hover:text-[#ccc]">Dashboard</Link>
</div>
</div>
</ThemedLayout>

View File

@@ -1,71 +1,244 @@
"use client";
import { useState, useEffect } from "react";
import { useState, useEffect, useCallback } from "react";
import Link from "next/link";
import { useAccount } from "@/contexts/AccountContext";
const NUMBERS_COUNT = 6;
const TICKET_KEY = "cyberlux-lotto-tickets-v1";
const DRAW_INTERVAL_MS = 24 * 60 * 60 * 1000;
const DRAW_EPOCH = new Date("2026-04-14T12:00:00Z").getTime();
function getNextDraw() {
const now = Date.now();
const elapsed = now - DRAW_EPOCH;
const cycles = Math.floor(elapsed / DRAW_INTERVAL_MS);
return DRAW_EPOCH + (cycles + 1) * DRAW_INTERVAL_MS;
}
function formatRemaining(ms: number) {
if (ms <= 0) return "DRAWING SOON";
const h = Math.floor(ms / 3600000);
const m = Math.floor((ms % 3600000) / 60000);
const s = Math.floor((ms % 60000) / 1000);
return `${h.toString().padStart(2, "0")}h ${m.toString().padStart(2, "0")}m ${s.toString().padStart(2, "0")}s`;
}
type Ticket = { nums: number[]; handle: string; ts: number };
function loadTickets(): Ticket[] {
if (typeof window === "undefined") return [];
try { return JSON.parse(localStorage.getItem(TICKET_KEY) ?? "[]") as Ticket[]; }
catch { return []; }
}
function saveTickets(t: Ticket[]) {
if (typeof window === "undefined") return;
localStorage.setItem(TICKET_KEY, JSON.stringify(t.slice(-50)));
}
export default function LotteryPage() {
const { user } = useAccount();
const [numbers, setNumbers] = useState<number[]>([]);
const [isDrawing, setIsDrawing] = useState(false);
const [lastWinner, setLastWinner] = useState<string>("0x7a...f2e1");
const [jackpot, setJackpot] = useState("1.24 BTC");
const [myTickets, setMyTickets] = useState<Ticket[]>([]);
const [remaining, setRemaining] = useState(0);
const [picked, setPicked] = useState<number[]>([]);
const [disclaimer, setDisclaimer] = useState(false);
const drawNumbers = () => {
useEffect(() => {
setMyTickets(loadTickets());
const tick = () => setRemaining(getNextDraw() - Date.now());
tick();
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, []);
const togglePick = (n: number) => {
if (picked.includes(n)) {
setPicked((p) => p.filter((x) => x !== n));
} else if (picked.length < NUMBERS_COUNT) {
setPicked((p) => [...p, n]);
}
};
const quickPick = () => {
const pool = Array.from({ length: 49 }, (_, i) => i + 1);
const result: number[] = [];
while (result.length < NUMBERS_COUNT) {
const idx = Math.floor(Math.random() * pool.length);
result.push(pool.splice(idx, 1)[0]!);
}
setPicked(result.sort((a, b) => a - b));
};
const submitTicket = useCallback(() => {
if (picked.length !== NUMBERS_COUNT) return;
const ticket: Ticket = {
nums: [...picked].sort((a, b) => a - b),
handle: user?.username ?? "anon",
ts: Date.now(),
};
const updated = [...myTickets, ticket];
setMyTickets(updated);
saveTickets(updated);
setPicked([]);
}, [picked, myTickets, user]);
const drawNumbers = useCallback(() => {
if (!disclaimer) { setDisclaimer(true); return; }
setIsDrawing(true);
setNumbers([]);
let count = 0;
const used = new Set<number>();
const interval = setInterval(() => {
setNumbers(prev => [...prev, Math.floor(Math.random() * 99) + 1]);
let n: number;
do { n = Math.floor(Math.random() * 49) + 1; } while (used.has(n));
used.add(n);
setNumbers((prev) => [...prev, n]);
count++;
if (count >= 6) {
if (count >= NUMBERS_COUNT) {
clearInterval(interval);
setIsDrawing(false);
}
}, 500);
};
}, 400);
}, [disclaimer]);
const drawn = numbers.length === NUMBERS_COUNT;
const myMatches = drawn
? myTickets.map((t) => ({ ticket: t, matches: t.nums.filter((n) => numbers.includes(n)).length }))
: [];
return (
<div className="min-h-screen bg-[#050505] text-[#ff00ff] font-mono p-8 flex flex-col items-center justify-center">
<div className="max-w-2xl w-full border-2 border-[#ff00ff]/30 bg-[#111] p-12 shadow-[0_0_50px_rgba(255,0,255,0.1)]">
<h1 className="text-5xl font-black text-center mb-4 tracking-widest uppercase italic">Shadow Lotto</h1>
<p className="text-center text-[#ff00ff]/60 mb-12">Provably fair. Completely anonymous. High stakes.</p>
<div className="grid grid-cols-2 gap-8 mb-12">
<div className="text-center border border-[#ff00ff]/20 p-4">
<div className="text-xs uppercase opacity-50 mb-1">Current Jackpot</div>
<div className="text-3xl font-bold text-white">{jackpot}</div>
<div className="min-h-screen bg-[#050505] text-[#ff00ff] font-mono p-6 flex flex-col items-center">
<div className="w-full max-w-2xl">
<div className="border-2 border-[#ff00ff]/30 bg-[#111] p-10 shadow-[0_0_50px_rgba(255,0,255,0.08)] mb-6">
<div className="mb-3 text-center text-[10px] uppercase tracking-widest text-[#ff00ff]/40">
Demo lotto prizes are LUX credits · no real-money payouts
</div>
<div className="text-center border border-[#ff00ff]/20 p-4">
<div className="text-xs uppercase opacity-50 mb-1">Last Winner</div>
<div className="text-sm font-bold text-white">{lastWinner}</div>
<h1 className="text-4xl font-black text-center mb-2 tracking-widest uppercase italic">Shadow Lotto</h1>
<p className="text-center text-[#ff00ff]/50 text-xs mb-8">
Pick 6 numbers (149) or Quick Pick. Daily demo draw matches earn LUX credits.
</p>
<div className="grid grid-cols-2 gap-6 mb-8 text-center">
<div className="border border-[#ff00ff]/20 p-4">
<div className="text-xs uppercase opacity-50 mb-1">Next Draw</div>
<div className="text-lg font-bold text-white">{formatRemaining(remaining)}</div>
</div>
<div className="border border-[#ff00ff]/20 p-4">
<div className="text-xs uppercase opacity-50 mb-1">Your Tickets</div>
<div className="text-lg font-bold text-white">{myTickets.length}</div>
</div>
</div>
<div className="flex justify-center gap-4 mb-12 h-16">
{/* Number picker */}
<div className="mb-6">
<div className="mb-3 flex items-center justify-between">
<span className="text-xs uppercase tracking-wider text-[#ff00ff]/70">
Pick {NUMBERS_COUNT} numbers · selected: {picked.length}/{NUMBERS_COUNT}
</span>
<button
type="button"
onClick={quickPick}
className="border border-[#ff00ff]/40 px-3 py-1 text-xs font-bold uppercase hover:bg-[#ff00ff]/10 transition-all"
>
Quick Pick
</button>
</div>
<div className="grid grid-cols-7 gap-1.5">
{Array.from({ length: 49 }, (_, i) => i + 1).map((n) => {
const isSelected = picked.includes(n);
const isDrawn = numbers.includes(n);
return (
<button
key={n}
type="button"
onClick={() => togglePick(n)}
className={`h-9 w-full border text-xs font-bold transition-all ${
isDrawn && drawn
? "border-[#ff00ff] bg-[#ff00ff]/30 text-white"
: isSelected
? "border-[#ff00ff] bg-[#ff00ff]/20 text-[#ff00ff]"
: "border-[#ff00ff]/15 bg-transparent text-[#ff00ff]/50 hover:border-[#ff00ff]/40"
}`}
>
{n}
</button>
);
})}
</div>
</div>
<div className="flex gap-3 mb-6">
<button
type="button"
onClick={submitTicket}
disabled={picked.length !== NUMBERS_COUNT}
className="flex-1 border-2 border-[#ff00ff] py-3 text-sm font-black uppercase transition-all hover:bg-[#ff00ff]/15 disabled:opacity-30"
>
Lock In Ticket ({picked.join("-") || "select 6"})
</button>
<button
type="button"
onClick={drawNumbers}
disabled={isDrawing}
className={`flex-1 py-3 text-sm font-black uppercase transition-all ${
isDrawing ? "bg-[#333] text-[#666] cursor-not-allowed" : "bg-[#ff00ff] text-black hover:bg-[#cc00cc]"
}`}
>
{isDrawing ? "Drawing…" : "Run Demo Draw"}
</button>
</div>
{disclaimer && !drawn && (
<div className="mb-4 border border-amber-500/50 bg-amber-950/30 p-4 text-xs text-amber-200 text-center">
This is a demo draw no real prizes are awarded. LUX credit prizes are in-app only.
<button type="button" onClick={drawNumbers} className="ml-3 underline font-bold">
OK, Draw Anyway
</button>
</div>
)}
{/* Draw result */}
{numbers.length > 0 && (
<div className="border border-[#ff00ff]/30 bg-[#ff00ff]/5 p-6">
<div className="mb-3 text-xs uppercase text-[#ff00ff]/60">Draw Result</div>
<div className="flex justify-center gap-3 flex-wrap">
{numbers.map((n, i) => (
<div key={i} className="w-16 h-16 border-2 border-[#ff00ff] flex items-center justify-center text-2xl font-bold bg-[#ff00ff]/10 animate-pulse">
<div
key={i}
className={`flex h-12 w-12 items-center justify-center border-2 font-black text-lg ${
isDrawing
? "border-[#ff00ff]/40 text-[#ff00ff]/60 animate-pulse"
: "border-[#ff00ff] text-white bg-[#ff00ff]/20"
}`}
>
{n}
</div>
))}
{Array.from({ length: 6 - numbers.length }).map((_, i) => (
<div key={i} className="w-16 h-16 border-2 border-[#ff00ff]/20 flex items-center justify-center text-2xl font-bold opacity-20">
?
</div>
{drawn && myMatches.length > 0 && (
<div className="mt-5 space-y-2">
<div className="text-xs uppercase text-[#ff00ff]/60">Your Ticket Results</div>
{myMatches.map((m, i) => (
<div key={i} className="flex items-center justify-between text-xs">
<span className="text-[#ff00ff]/60">{m.ticket.nums.join("-")}</span>
<span className={m.matches >= 3 ? "text-[#ff00ff] font-bold" : "text-white/40"}>
{m.matches} match{m.matches !== 1 ? "es" : ""}
{m.matches >= 6 ? " — JACKPOT (LUX prize)" : m.matches >= 4 ? " — LUX reward" : ""}
</span>
</div>
))}
</div>
)}
</div>
)}
</div>
<button
onClick={drawNumbers}
disabled={isDrawing}
className={`w-full py-4 text-xl font-bold uppercase tracking-widest transition-all
${isDrawing ? "bg-[#333] text-[#666] cursor-not-allowed" : "bg-[#ff00ff] text-black hover:bg-[#cc00cc] hover:shadow-[0_0_30px_#ff00ff]/40"}
`}
>
{isDrawing ? "Drawing..." : "Buy Ticket (0.0001 BTC)"}
</button>
<div className="mt-12 text-[10px] text-[#ff00ff]/40 leading-relaxed">
* All draws are finalized via the Shadow Protocol. Tickets are non-refundable.
Winners are paid out within 3 confirmations. Good luck, traveler.
<div className="text-center text-[10px] text-[#ff00ff]/25 leading-relaxed">
Shadow Lotto is a demonstration game. No real-money gambling. LUX credits are in-app only.{" "}
<Link href="/inner-circle" className="text-[#ff00ff]/40 underline hover:text-[#ff00ff]/60">Inner Circle </Link>
</div>
</div>
</div>

View File

@@ -1,74 +1,297 @@
"use client";
import { useState, useEffect, useRef } from "react";
import { useState, useEffect, useRef, useCallback } from "react";
import Link from "next/link";
export default function DarknetPingPong() {
const [ballPos, setBallPos] = useState({ x: 50, y: 50 });
const [isHaunted, setIsHaunted] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const FIELD_W = 600;
const FIELD_H = 400;
const BALL_R = 12;
const PADDLE_W = 12;
const PADDLE_H = 70;
const CPU_SPEED = 3.2;
const INITIAL_SPEED = 4.5;
// Simulate shared state with a simple interval or WebSocket-like behavior
// In a real app, this would use Pusher, Socket.io, or a backend
useEffect(() => {
const interval = setInterval(() => {
// Random "haunted" movement to simulate other users or ghosts
if (Math.random() > 0.95) {
setBallPos(prev => ({
x: Math.max(10, Math.min(90, prev.x + (Math.random() - 0.5) * 20)),
y: Math.max(10, Math.min(90, prev.y + (Math.random() - 0.5) * 20)),
}));
setIsHaunted(true);
setTimeout(() => setIsHaunted(false), 500);
type Vec = { x: number; y: number };
type GameState = "idle" | "playing" | "paused" | "over";
function clamp(v: number, lo: number, hi: number) {
return Math.max(lo, Math.min(hi, v));
}
}, 2000);
return () => clearInterval(interval);
const HIGH_SCORE_KEY = "cyberlux-pong-hs";
function getHighScore() {
if (typeof window === "undefined") return 0;
return parseInt(localStorage.getItem(HIGH_SCORE_KEY) ?? "0", 10);
}
function setHighScore(n: number) {
if (typeof window !== "undefined") localStorage.setItem(HIGH_SCORE_KEY, String(n));
}
export default function PongPage() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const stateRef = useRef<GameState>("idle");
const ballRef = useRef<Vec>({ x: FIELD_W / 2, y: FIELD_H / 2 });
const velRef = useRef<Vec>({ x: INITIAL_SPEED, y: INITIAL_SPEED });
const playerYRef = useRef((FIELD_H - PADDLE_H) / 2);
const cpuYRef = useRef((FIELD_H - PADDLE_H) / 2);
const scoreRef = useRef({ player: 0, cpu: 0 });
const rafRef = useRef<number>(0);
const mouseYRef = useRef(FIELD_H / 2);
const [displayScore, setDisplayScore] = useState({ player: 0, cpu: 0 });
const [gameState, setGameState] = useState<GameState>("idle");
const [highScore, setHighScoreState] = useState(0);
const [rally, setRally] = useState(0);
const rallyRef = useRef(0);
useEffect(() => {
setHighScoreState(getHighScore());
}, []);
const handleBallClick = (e: React.MouseEvent) => {
if (!containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * 100;
const y = ((e.clientY - rect.top) / rect.height) * 100;
// Move ball to click position
setBallPos({ x, y });
// In a real app, you'd emit this to a server:
// socket.emit('move_ball', { x, y });
const reset = useCallback(() => {
ballRef.current = { x: FIELD_W / 2, y: FIELD_H / 2 };
const angle = (Math.random() * Math.PI) / 3 - Math.PI / 6;
const dir = Math.random() > 0.5 ? 1 : -1;
velRef.current = {
x: INITIAL_SPEED * dir * Math.cos(angle),
y: INITIAL_SPEED * Math.sin(angle),
};
rallyRef.current = 0;
setRally(0);
}, []);
const draw = useCallback(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
// Background
ctx.fillStyle = "#050510";
ctx.fillRect(0, 0, FIELD_W, FIELD_H);
// Center line
ctx.setLineDash([8, 8]);
ctx.strokeStyle = "rgba(0,255,200,0.1)";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(FIELD_W / 2, 0);
ctx.lineTo(FIELD_W / 2, FIELD_H);
ctx.stroke();
ctx.setLineDash([]);
// Paddles
const drawPaddle = (x: number, y: number, color: string) => {
ctx.shadowColor = color;
ctx.shadowBlur = 12;
ctx.fillStyle = color;
ctx.fillRect(x, y, PADDLE_W, PADDLE_H);
ctx.shadowBlur = 0;
};
drawPaddle(8, playerYRef.current, "#00ffcc");
drawPaddle(FIELD_W - 8 - PADDLE_W, cpuYRef.current, "#ff00ff");
// Ball
ctx.shadowColor = "#00ffcc";
ctx.shadowBlur = 20;
ctx.fillStyle = "#fff";
ctx.beginPath();
ctx.arc(ballRef.current.x, ballRef.current.y, BALL_R, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
// Score overlay
ctx.fillStyle = "rgba(0,255,200,0.15)";
ctx.font = "bold 36px monospace";
ctx.textAlign = "center";
ctx.fillText(String(scoreRef.current.player), FIELD_W / 4, 50);
ctx.fillStyle = "rgba(255,0,255,0.15)";
ctx.fillText(String(scoreRef.current.cpu), (FIELD_W * 3) / 4, 50);
}, []);
const tick = useCallback(() => {
if (stateRef.current !== "playing") return;
// Move ball
const b = ballRef.current;
const v = velRef.current;
b.x += v.x;
b.y += v.y;
// Wall bounce top/bottom
if (b.y - BALL_R <= 0) { b.y = BALL_R; v.y = Math.abs(v.y); }
if (b.y + BALL_R >= FIELD_H) { b.y = FIELD_H - BALL_R; v.y = -Math.abs(v.y); }
// CPU paddle AI
const cpuCenter = cpuYRef.current + PADDLE_H / 2;
if (cpuCenter < b.y - 4) cpuYRef.current = clamp(cpuYRef.current + CPU_SPEED, 0, FIELD_H - PADDLE_H);
if (cpuCenter > b.y + 4) cpuYRef.current = clamp(cpuYRef.current - CPU_SPEED, 0, FIELD_H - PADDLE_H);
// Player paddle follow mouse
playerYRef.current = clamp(mouseYRef.current - PADDLE_H / 2, 0, FIELD_H - PADDLE_H);
// Player paddle hit
if (b.x - BALL_R <= 8 + PADDLE_W && b.y >= playerYRef.current && b.y <= playerYRef.current + PADDLE_H && v.x < 0) {
b.x = 8 + PADDLE_W + BALL_R;
const hitPos = (b.y - playerYRef.current) / PADDLE_H - 0.5;
const speed = Math.min(Math.sqrt(v.x ** 2 + v.y ** 2) + 0.15, 10);
v.x = Math.abs(speed * Math.cos(hitPos * Math.PI * 0.6));
v.y = speed * Math.sin(hitPos * Math.PI * 0.6);
rallyRef.current++;
setRally(rallyRef.current);
}
// CPU paddle hit
if (b.x + BALL_R >= FIELD_W - 8 - PADDLE_W && b.y >= cpuYRef.current && b.y <= cpuYRef.current + PADDLE_H && v.x > 0) {
b.x = FIELD_W - 8 - PADDLE_W - BALL_R;
const hitPos = (b.y - cpuYRef.current) / PADDLE_H - 0.5;
const speed = Math.sqrt(v.x ** 2 + v.y ** 2);
v.x = -Math.abs(speed * Math.cos(hitPos * Math.PI * 0.6));
v.y = speed * Math.sin(hitPos * Math.PI * 0.6);
rallyRef.current++;
setRally(rallyRef.current);
}
// Scoring
if (b.x < 0) {
scoreRef.current.cpu++;
setDisplayScore({ ...scoreRef.current });
if (scoreRef.current.cpu >= 7) {
stateRef.current = "over";
setGameState("over");
const hs = getHighScore();
if (rallyRef.current > hs) { setHighScore(rallyRef.current); setHighScoreState(rallyRef.current); }
} else reset();
}
if (b.x > FIELD_W) {
scoreRef.current.player++;
setDisplayScore({ ...scoreRef.current });
if (scoreRef.current.player >= 7) {
stateRef.current = "over";
setGameState("over");
const hs = getHighScore();
if (rallyRef.current > hs) { setHighScore(rallyRef.current); setHighScoreState(rallyRef.current); }
} else reset();
}
draw();
rafRef.current = requestAnimationFrame(tick);
}, [draw, reset]);
const startGame = useCallback(() => {
scoreRef.current = { player: 0, cpu: 0 };
setDisplayScore({ player: 0, cpu: 0 });
stateRef.current = "playing";
setGameState("playing");
reset();
if (rafRef.current) cancelAnimationFrame(rafRef.current);
rafRef.current = requestAnimationFrame(tick);
}, [tick, reset]);
useEffect(() => {
draw();
return () => { if (rafRef.current) cancelAnimationFrame(rafRef.current); };
}, [draw]);
const handleMouseMove = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
const rect = canvasRef.current?.getBoundingClientRect();
if (!rect) return;
const scaleY = FIELD_H / rect.height;
mouseYRef.current = (e.clientY - rect.top) * scaleY;
}, []);
const handleTouchMove = useCallback((e: React.TouchEvent<HTMLCanvasElement>) => {
e.preventDefault();
const rect = canvasRef.current?.getBoundingClientRect();
if (!rect || !e.touches[0]) return;
const scaleY = FIELD_H / rect.height;
mouseYRef.current = (e.touches[0].clientY - rect.top) * scaleY;
}, []);
return (
<div
ref={containerRef}
className="relative w-full h-[600px] bg-[url('https://images.unsplash.com/photo-1533090161767-e6ffed986c88?q=80&w=2069&auto=format&fit=crop')] bg-cover bg-center border-8 border-[#3d2b1f] shadow-2xl overflow-hidden cursor-crosshair"
style={{ backgroundColor: "#1a1a1a" }}
>
<div className="absolute inset-0 bg-black/40 pointer-events-none" />
{/* The Haunted Ball */}
<div
onClick={handleBallClick}
className={`absolute w-16 h-16 rounded-full cursor-pointer transition-all duration-500 ease-out
${isHaunted ? "scale-125 blur-sm" : "scale-100"}
shadow-[0_0_30px_rgba(255,255,255,0.3)]
`}
style={{
left: `${ballPos.x}%`,
top: `${ballPos.y}%`,
transform: "translate(-50%, -50%)",
background: "radial-gradient(circle at 30% 30%, #555, #000)",
border: "2px solid rgba(255,255,255,0.1)"
}}
>
<div className="absolute inset-0 flex items-center justify-center opacity-20">
<span className="text-white text-[10px] uppercase tracking-widest">void</span>
<div className="flex min-h-screen flex-col items-center justify-center bg-[#050510] p-6 font-mono text-white">
<div className="w-full max-w-[620px]">
<div className="mb-4 flex items-center justify-between">
<div>
<h1 className="text-xl font-black uppercase tracking-widest text-neon-cyan">VOID PONG</h1>
<p className="text-[10px] text-white/30">Move mouse over field to control paddle. First to 7.</p>
</div>
<div className="text-right text-xs text-white/40">
<div>Rally record: <span className="text-neon-cyan">{highScore}</span></div>
{rally > 0 && gameState === "playing" && (
<div>Current rally: <span className="text-neon-purple">{rally}</span></div>
)}
</div>
</div>
<div className="absolute bottom-4 left-4 text-white/50 font-mono text-xs uppercase tracking-widest">
Shared Entity #001 - Darknet Ping Pong
{gameState !== "idle" && (
<div className="mb-3 flex items-center justify-between text-sm">
<span className="text-neon-cyan font-bold">YOU {displayScore.player}</span>
<span className="text-xs text-white/30">vs</span>
<span className="text-neon-purple font-bold">{displayScore.cpu} CPU</span>
</div>
)}
<canvas
ref={canvasRef}
width={FIELD_W}
height={FIELD_H}
onMouseMove={handleMouseMove}
onTouchMove={handleTouchMove}
className="w-full rounded-lg border border-neon-cyan/20 cursor-none"
style={{ touchAction: "none" }}
/>
{gameState === "idle" && (
<div className="mt-6 text-center">
<button
type="button"
onClick={startGame}
className="rounded-full border-2 border-neon-cyan px-8 py-3 font-black uppercase text-neon-cyan hover:bg-neon-cyan/10 transition-all"
>
Start Game
</button>
<p className="mt-3 text-xs text-white/30">Move your mouse over the field to control the left paddle.</p>
</div>
)}
{gameState === "playing" && (
<div className="mt-4 text-center text-xs text-white/20">
Rally: {rally} hits
</div>
)}
{gameState === "over" && (
<div className="mt-6 text-center">
<div className="mb-3 text-2xl font-black uppercase">
{displayScore.player >= 7 ? (
<span className="text-neon-cyan">You Win</span>
) : (
<span className="text-neon-purple">CPU Wins</span>
)}
</div>
<div className="mb-4 text-sm text-white/50">
Score: {displayScore.player} {displayScore.cpu} · Rally: {rallyRef.current}
{rallyRef.current > 0 && rallyRef.current >= highScore && (
<span className="ml-2 text-neon-cyan">New record!</span>
)}
</div>
<button
type="button"
onClick={startGame}
className="rounded-full border-2 border-neon-cyan px-8 py-3 font-black uppercase text-neon-cyan hover:bg-neon-cyan/10 transition-all"
>
Play Again
</button>
</div>
)}
<div className="mt-6 flex justify-center gap-6 text-[10px] text-white/20">
<Link href="/" className="hover:text-white/50">Hub</Link>
<Link href="/arcade" className="hover:text-white/50">Arcade</Link>
<Link href="/drops" className="hover:text-white/50">Shadow Lotto</Link>
</div>
</div>
</div>
);

View File

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

View File

@@ -1,10 +1,6 @@
"use client";
import Link from "next/link";
import { PLACEHOLDER_ONION_URL } from "@/lib/placeholderOnion";
/** @deprecated use PLACEHOLDER_ONION_URL */
export const HIDDEN_WIKI_PLACEHOLDER_ONION = PLACEHOLDER_ONION_URL;
type WikiItem = {
title: string;
@@ -38,7 +34,13 @@ const ENTRIES: { cat: string; items: WikiItem[] }[] = [
{
title: "Account & dashboard",
note: "Client-side vault unlock; dashboard aggregates forum, exchange, and barter handles.",
href: "/sign-in",
href: "/dashboard",
external: false,
},
{
title: "Add funds (Bitcoin)",
note: "Verified on-chain deposits credit USD to your signed-in handle.",
href: "/account/add-funds",
external: false,
},
{
@@ -59,51 +61,51 @@ const ENTRIES: { cat: string; items: WikiItem[] }[] = [
cat: "Directories & indexes",
items: [
{
title: "OnionDir (mirror slot 1)",
note: "Template row — paste a signed directory .onion after you verify the key.",
href: PLACEHOLDER_ONION_URL,
external: true,
title: "Void crawler",
note: "Search this deployments signed routes.",
href: "/search",
external: false,
},
{
title: "Glass index (mirror slot 2)",
note: "Awaiting mirror hostname.",
href: PLACEHOLDER_ONION_URL,
external: true,
title: "Link garden",
note: "Curated internal link board.",
href: "/links",
external: false,
},
{
title: "Card-file aggregator (slot 3)",
note: "Awaiting mirror hostname.",
href: PLACEHOLDER_ONION_URL,
external: true,
title: "Darknet Atlas",
note: "Taxonomy with links back into CyberLux surfaces.",
href: "/darknet-atlas",
external: false,
},
{
title: "Fresh mirror list (slot 4)",
note: "Awaiting mirror hostname.",
href: PLACEHOLDER_ONION_URL,
external: true,
title: "Onion mirror map",
note: "Per-host identity + portable account bundle.",
href: "/account/hidden-services",
external: false,
},
],
},
{
cat: "Markets & trading (unverified)",
cat: "Markets & trading",
items: [
{
title: "Market slot A",
note: "Replace with your verified link + PGP proof.",
href: PLACEHOLDER_ONION_URL,
external: true,
title: "Market catalog",
note: "Primary storefront SKUs.",
href: "/market",
external: false,
},
{
title: "Market slot B",
note: "Replace with your verified link + PGP proof.",
href: PLACEHOLDER_ONION_URL,
external: true,
title: "Classifieds (exchange)",
note: "WTS / WTB listings on this host.",
href: "/exchange",
external: false,
},
{
title: "Market slot C",
note: "Replace with your verified link + PGP proof.",
href: PLACEHOLDER_ONION_URL,
external: true,
title: "Vendor hall",
note: "Seller index and stall applications.",
href: "/vendors",
external: false,
},
],
},
@@ -111,22 +113,22 @@ const ENTRIES: { cat: string; items: WikiItem[] }[] = [
cat: "Comms & privacy",
items: [
{
title: "Secure chat relay (slot 1)",
note: "Self-hosted bridge — verify certs when you swap URL.",
href: PLACEHOLDER_ONION_URL,
external: true,
title: "Messages (stub)",
note: "Local UI shell — no remote mailbox yet.",
href: "/messages",
external: false,
},
{
title: "VPN / review matrix (slot 2)",
note: "Crowd-sourced; assume bias.",
href: PLACEHOLDER_ONION_URL,
external: true,
title: "Trust center",
note: "Mirror hygiene and safety copy.",
href: "/trust",
external: false,
},
{
title: "Encrypted mail drop (slot 3)",
note: "Hostname pending operator upload.",
href: PLACEHOLDER_ONION_URL,
external: true,
title: "Support",
note: "Contact and routing notes.",
href: "/support",
external: false,
},
],
},
@@ -134,16 +136,16 @@ const ENTRIES: { cat: string; items: WikiItem[] }[] = [
cat: "Hosting & infrastructure",
items: [
{
title: "Bulletproof-style host (slot 1)",
note: "Rumor link — due diligence on you; hosting laws vary by territory.",
href: PLACEHOLDER_ONION_URL,
external: true,
title: "Launch / deploy notes",
note: "Tor + nginx + verify script — operator entry for the CyberLux onion.",
href: "/launch",
external: false,
},
{
title: "Collocation / VPS gossip (slot 2)",
note: "Hostname pending operator upload.",
href: PLACEHOLDER_ONION_URL,
external: true,
title: "Security analysis",
note: "In-app security framing.",
href: "/security-analysis",
external: false,
},
],
},
@@ -151,16 +153,16 @@ const ENTRIES: { cat: string; items: WikiItem[] }[] = [
cat: "Forums & social",
items: [
{
title: "Forum mirror (slot 1)",
note: "Hostname pending operator upload.",
href: PLACEHOLDER_ONION_URL,
external: true,
title: "Void Aggregate (forums)",
note: "Ringed boards on this build.",
href: "/forum",
external: false,
},
{
title: "Chan / imageboard mirror (slot 2)",
note: "Hostname pending operator upload.",
href: PLACEHOLDER_ONION_URL,
external: true,
title: "Chatter archive",
note: "Hub commentary index.",
href: "/chatter",
external: false,
},
],
},
@@ -174,7 +176,8 @@ const ENTRIES: { cat: string; items: WikiItem[] }[] = [
{ title: "Classifieds", note: "WTS / WTB — same deployment.", href: "/exchange", external: false },
{ title: "Ash Pit (barter)", note: "Have / want swap board — dark trading floor.", href: "/barter", external: false },
{ title: "Link garden", note: "Alternate directory layout.", href: "/links", external: false },
{ title: "Checkout", note: "BTC settlement & balance.", href: "/checkout", external: false },
{ title: "Checkout", note: "Bitcoin-funded USD balance.", href: "/checkout", external: false },
{ title: "Add funds", note: "Deposit flow + txid verify.", href: "/account/add-funds", external: false },
],
},
];
@@ -224,6 +227,12 @@ export default function HiddenWikiPage() {
>
All mirrors
</Link>
<Link
href="/launch"
className="border border-[#2a3444] bg-[#0f141c] px-3 py-1.5 text-[#94a8b8] hover:border-[#3d5a80]/50"
>
Launch
</Link>
<Link
href="/syndicate"
className="border border-[#2a3444] bg-[#0f141c] px-3 py-1.5 text-[#94a8b8] hover:border-[#3d5a80]/50"

197
app/hosting/page.tsx Normal file
View 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&apos;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>
</>
);
}

View File

@@ -1,86 +1,226 @@
"use client";
import { useState } from "react";
import Link from "next/link";
import ThemedLayout from "@/components/layouts/ThemedLayout";
import { useAccount } from "@/contexts/AccountContext";
import { useWallet } from "@/contexts/WalletContext";
export default function MembershipPage() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [user, setUser] = useState("");
const [pass, setPass] = useState("");
const MEMBERSHIP_TIERS = [
{ name: "Associate", minLux: 0, description: "Access to the channel digest and ring updates." },
{ name: "Operative", minLux: 500, description: "Priority listing visibility and vault drop previews." },
{ name: "Handler", minLux: 2000, description: "Early market access, barter priority lane, vendor fast-track." },
{ name: "Principal", minLux: 8000, description: "All tiers plus operator-level forum ring keys and direct relay contact." },
];
const handleLogin = (e: React.FormEvent) => {
e.preventDefault();
if (user === "admin" && pass === "shadow") {
setIsLoggedIn(true);
} else {
alert("Invalid credentials. Access denied.");
function getTier(lux: number) {
let result = MEMBERSHIP_TIERS[0]!;
for (const t of MEMBERSHIP_TIERS) {
if (lux >= t.minLux) result = t;
}
return result;
}
};
const panel = "border-[10px] border-[#e5e5e5] bg-[#161616] p-12 shadow-[20px_20px_0_rgba(0,0,0,0.8)] text-[#f0f0f0]";
const DIGEST = [
{ tag: "MARKET", text: "New entropy dongle batch listed — vendor @relay_op, verified PGP.", href: "/market" },
{ tag: "FORUM", text: "Thread on OPSEC hygiene for multi-site handles is trending in Ring IV.", href: "/forum" },
{ tag: "DROPS", text: "Next scheduled drop in ~18h. Add /drops to your watchlist.", href: "/drops" },
{ tag: "EXCHANGE", text: "WTB listing for XMR-to-credit bridge services posted 4h ago.", href: "/exchange" },
{ tag: "VAULT", text: "Your receipts and earned keys live in the Vault — check after checkout.", href: "/vault" },
];
export default function InnerCirclePage() {
const { user, hydrated } = useAccount();
const { luxCredits } = useWallet();
const tier = getTier(luxCredits);
const [tab, setTab] = useState<"digest" | "tiers" | "guide">("digest");
if (!hydrated) {
return (
<ThemedLayout theme="brutalist" title="The Inner Circle">
<div className="mx-auto max-w-md pt-20 text-center font-mono text-sm text-foreground/50">
Loading
</div>
</ThemedLayout>
);
}
if (!user) {
return (
<ThemedLayout theme="brutalist" title="The Inner Circle">
<div className="mx-auto max-w-md pt-20">
{!isLoggedIn ? (
<div className={panel}>
<h2 className="mb-8 text-4xl font-black uppercase italic text-[#fafafa]">Members Only</h2>
<p className="mb-8 text-xs font-bold text-[#b0b0b0]">
Access to the premium signal requires an active subscription. Pay 0.01 BTC to{" "}
<span className="bg-[#262626] px-1 text-[#7cfc9a]">0x594f1Cf2A72b3f785fcB6ABdFa73B6D76FcC22b8</span> to
receive your key.
<div className="border-[8px] border-[#e5e5e5] bg-[#161616] p-12 shadow-[16px_16px_0_rgba(0,0,0,0.8)] text-[#f0f0f0]">
<h2 className="mb-4 text-4xl font-black uppercase italic">Members Only</h2>
<p className="mb-8 text-sm text-[#aaa]">
The Inner Circle is gated by your CyberLux handle. Sign in and earn LUX credits through purchases and
forum activity to unlock higher tiers.
</p>
<form onSubmit={handleLogin} className="space-y-6">
<div>
<label className="mb-2 block text-xs font-black uppercase text-[#a0a0a0]">Username</label>
<input
type="text"
value={user}
onChange={(e) => setUser(e.target.value)}
className="w-full border-4 border-[#404040] bg-[#0a0a0a] p-3 font-bold text-[#f0f0f0] focus:border-[#7cfc9a] focus:outline-none"
/>
<div className="space-y-4">
<Link
href="/sign-in?next=/inner-circle"
className="block w-full bg-[#f0f0f0] py-4 text-center font-black uppercase text-black transition-colors hover:bg-[#7cfc9a]"
>
Sign In
</Link>
<Link
href="/sign-up?next=/inner-circle"
className="block w-full border-4 border-[#e5e5e5] py-4 text-center font-black uppercase hover:border-[#7cfc9a]"
>
Create Handle
</Link>
</div>
<div>
<label className="mb-2 block text-xs font-black uppercase text-[#a0a0a0]">Access Key</label>
<input
type="password"
value={pass}
onChange={(e) => setPass(e.target.value)}
className="w-full border-4 border-[#404040] bg-[#0a0a0a] p-3 font-bold text-[#f0f0f0] focus:border-[#7cfc9a] focus:outline-none"
/>
</div>
<button className="w-full bg-[#f0f0f0] py-4 font-black uppercase text-black transition-colors hover:bg-[#7cfc9a]">
Enter the Circle
</button>
</form>
</div>
) : (
<div className={panel}>
<h2 className="mb-8 text-4xl font-black uppercase text-[#fafafa]">Welcome, Initiate</h2>
<div className="space-y-6">
<div className="border-4 border-[#854d0e] bg-[#1c1410] p-4">
<h3 className="font-black uppercase text-[#fbbf24]">Today&apos;s Signal</h3>
<p className="mt-2 text-sm text-[#d6cbb8]">Buy $BTC at 64,200. Target 68,000. Stop loss 63,500.</p>
</div>
<div className="border-4 border-[#14532d] bg-[#0f1612] p-4">
<h3 className="font-black uppercase text-[#4ade80]">Private Leak</h3>
<p className="mt-2 text-sm text-[#d6cbb8]">
New database from &quot;GlobalCorp&quot; available in the vault.
</p>
</div>
<button onClick={() => setIsLoggedIn(false)} className="text-xs font-black uppercase text-[#a0a0a0] underline">
Logout
</button>
</div>
</div>
)}
<div className="mt-12 flex items-center justify-center gap-4 opacity-60">
<div className="flex h-12 w-12 items-center justify-center border-4 border-[#e5e5e5] font-black text-[#fafafa]">E</div>
<div className="text-[10px] font-black uppercase leading-tight text-[#888]">
Escrow Assured
<br />
by ShadowGuard
<div className="mt-10 border-t border-white/10 pt-6">
<p className="text-[10px] uppercase text-[#666]">Tier access based on LUX balance no paid gate</p>
</div>
</div>
</div>
</ThemedLayout>
);
}
return (
<ThemedLayout theme="brutalist" title="The Inner Circle">
<div className="mx-auto max-w-3xl pt-12 pb-20 px-4">
<div className="border-[8px] border-[#e5e5e5] bg-[#161616] shadow-[16px_16px_0_rgba(0,0,0,0.8)] text-[#f0f0f0]">
{/* Header */}
<div className="border-b-4 border-[#e5e5e5] bg-[#111] px-8 py-6">
<div className="flex flex-wrap items-center justify-between gap-4">
<div>
<h2 className="text-3xl font-black uppercase italic">Welcome, @{user.username}</h2>
<p className="mt-1 text-sm text-[#aaa]">
Tier:{" "}
<span className="font-black text-[#7cfc9a]">{tier.name}</span>
{" · "}
<span className="text-[#7cfc9a]">{luxCredits.toLocaleString()} LUX</span>
</p>
</div>
<div className="text-right text-[10px] uppercase text-[#666]">
<div className="text-2xl font-black text-[#7cfc9a]">{tier.name}</div>
<div>{tier.description}</div>
</div>
</div>
</div>
{/* Tier progress */}
<div className="border-b-4 border-[#3a3a3a] px-8 py-4">
<div className="flex gap-2">
{MEMBERSHIP_TIERS.map((t, i) => {
const reached = luxCredits >= t.minLux;
const active = t.name === tier.name;
return (
<div
key={t.name}
className={`flex-1 border-2 py-2 text-center text-[10px] font-black uppercase transition-colors ${
active
? "border-[#7cfc9a] bg-[#7cfc9a]/15 text-[#7cfc9a]"
: reached
? "border-[#666] text-[#aaa]"
: "border-[#333] text-[#555]"
}`}
>
<div>{t.name}</div>
<div className="font-normal">{t.minLux.toLocaleString()} LUX</div>
</div>
);
})}
</div>
</div>
{/* Tabs */}
<div className="flex border-b-4 border-[#3a3a3a]">
{(["digest", "tiers", "guide"] as const).map((t) => (
<button
key={t}
type="button"
onClick={() => setTab(t)}
className={`flex-1 py-3 text-[11px] font-black uppercase transition-colors ${
tab === t ? "bg-[#7cfc9a]/15 text-[#7cfc9a]" : "text-[#888] hover:text-[#ccc]"
}`}
>
{t === "digest" ? "Signal Digest" : t === "tiers" ? "Tier Benefits" : "Operations Guide"}
</button>
))}
</div>
{/* Content */}
<div className="p-8">
{tab === "digest" && (
<div className="space-y-4">
<p className="text-[11px] uppercase text-[#666]">Current hub signals · real-time activity</p>
{DIGEST.map((d) => (
<Link
key={d.tag}
href={d.href}
className="flex items-start gap-4 border-2 border-[#333] bg-[#0d0d0d] p-4 transition-all hover:border-[#7cfc9a]/40"
>
<span className="mt-0.5 shrink-0 rounded bg-[#7cfc9a]/15 px-2 py-0.5 text-[10px] font-black uppercase text-[#7cfc9a]">
{d.tag}
</span>
<p className="text-sm text-[#ddd]">{d.text}</p>
</Link>
))}
</div>
)}
{tab === "tiers" && (
<div className="space-y-4">
<p className="text-[11px] uppercase text-[#666]">Earn LUX via purchases and activity</p>
{MEMBERSHIP_TIERS.map((t) => {
const reached = luxCredits >= t.minLux;
return (
<div
key={t.name}
className={`border-2 p-6 ${reached ? "border-[#7cfc9a]/40 bg-[#0f1a0f]" : "border-[#333] bg-[#0d0d0d] opacity-60"}`}
>
<div className="flex items-center justify-between">
<div className="text-xl font-black">{t.name}</div>
<div className="text-sm font-bold text-[#7cfc9a]">{t.minLux.toLocaleString()} LUX</div>
</div>
<p className="mt-2 text-sm text-[#aaa]">{t.description}</p>
{!reached && (
<p className="mt-2 text-xs text-[#666]">
Need {(t.minLux - luxCredits).toLocaleString()} more LUX
</p>
)}
</div>
);
})}
</div>
)}
{tab === "guide" && (
<div className="space-y-6 text-sm text-[#ccc]">
<p className="text-[11px] uppercase text-[#666]">Operational guidelines</p>
<div>
<h4 className="mb-2 font-black uppercase text-[#f0f0f0]">Earning LUX</h4>
<p className="text-[#aaa] leading-relaxed">
LUX credits are earned on every purchase at checkout. Spend at the Mixer (coin-blending theatre),
or hold to maintain tier status. LUX is stored per-handle in your browser export your account
bundle to carry it across devices.
</p>
</div>
<div>
<h4 className="mb-2 font-black uppercase text-[#f0f0f0]">Adding Funds</h4>
<p className="text-[#aaa] leading-relaxed">
Fund your account via Bitcoin at{" "}
<Link href="/account/add-funds" className="text-[#7cfc9a] underline">
/account/add-funds
</Link>
. After one confirmation, paste the txid USD credit is applied to your handle at current spot.
</p>
</div>
<div>
<h4 className="mb-2 font-black uppercase text-[#f0f0f0]">OPSEC basics</h4>
<p className="text-[#aaa] leading-relaxed">
Use Tor Browser on .onion deployments. Export your account bundle from{" "}
<Link href="/dashboard" className="text-[#7cfc9a] underline">
/dashboard
</Link>{" "}
before clearing site data. Verify mirror PGP each session phishing clones exist.
</p>
</div>
</div>
)}
</div>
</div>
</div>

267
app/labs/page.tsx Normal file
View 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>
</>
);
}

63
app/launch/page.tsx Normal file
View File

@@ -0,0 +1,63 @@
import Link from "next/link";
import Navbar from "@/components/Navbar";
import ParticleBackground from "@/components/ParticleBackground";
export default function LaunchPage() {
return (
<>
<ParticleBackground />
<Navbar />
<main className="relative z-10 mx-auto max-w-3xl px-4 pb-32 pt-28">
<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 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>
<section className="mt-12 space-y-6 rounded-2xl border border-white/10 bg-black/40 p-8 backdrop-blur">
<h2 className="font-orbitron text-lg font-bold text-neon-purple">1. Build & wire Tor + nginx</h2>
<pre className="overflow-x-auto rounded-xl bg-black/60 p-4 text-xs leading-relaxed text-neon-cyan/90">
{`cd ~/cyberlux
./start.sh
# or prepare only, then systemd:
# CYBERLUX_PREPARE_ONLY=1 ./start.sh
# sudo CYBERLUX_USER=$USER bash scripts/install-systemd.sh`}
</pre>
</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">2. Run the app</h2>
<p className="text-sm text-foreground/70">
Foreground: <code className="text-neon-green">npm run start:onion</code> · Boot:{" "}
<code className="text-neon-green">sudo systemctl start cyberlux.service</code>
</p>
</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 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 # CyberLux .onion URL
sudo bash scripts/list-onion-urls.sh # if hostname files need root
npm run onions:status # URLs + loopback checks`}
</pre>
</section>
<p className="mt-10 text-sm text-foreground/55">
Full reference: <code className="text-foreground/80">DEPLOY.md</code> in the repo root (Torrc, key backup, firewall).
</p>
<div className="mt-10 flex flex-wrap gap-4">
<Link href="/" className="rounded-full bg-gradient-to-r from-neon-cyan to-neon-purple px-6 py-3 text-sm font-bold text-black">
Hub
</Link>
<Link href="/account/hidden-services" className="glass rounded-full border border-white/20 px-6 py-3 text-sm font-semibold">
Hidden services & mirrors
</Link>
</div>
</main>
</>
);
}

View File

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

View File

@@ -1,308 +1,213 @@
"use client";
import Link from "next/link";
import { useState } from "react";
import ThemedLayout from "@/components/layouts/ThemedLayout";
import { LINK_GARDEN_INTERNAL } from "@/lib/cyberluxSitemap";
type Resource = {
name: string;
description: string;
href: string;
category: string;
trust: "internal" | "clearnet" | "onion-required";
icon: string;
external: boolean;
};
const RESOURCES: Resource[] = [
{
name: "Tor Project",
description: "Official documentation for the Tor anonymity network and Tor Browser. Start here for anything Tor-related.",
href: "https://www.torproject.org/",
category: "Infrastructure",
trust: "clearnet",
icon: "🧅",
external: true,
},
{
name: "EFF — Surveillance Self-Defense",
description: "Electronic Frontier Foundation's guide to protecting yourself from digital surveillance.",
href: "https://ssd.eff.org/",
category: "OPSEC",
trust: "clearnet",
icon: "🛡️",
external: true,
},
{
name: "Tails OS",
description: "Privacy-focused amnesic operating system that leaves no trace. Runs from a USB drive.",
href: "https://tails.boum.org/",
category: "Infrastructure",
trust: "clearnet",
icon: "💾",
external: true,
},
{
name: "Privacy Guides",
description: "Community-maintained recommendations for privacy software, services, and tools across every category.",
href: "https://www.privacyguides.org/",
category: "Reference",
trust: "clearnet",
icon: "📚",
external: true,
},
{
name: "SecureDrop",
description: "Open-source whistleblower submission platform used by major news organizations.",
href: "https://securedrop.org/",
category: "Whistleblowing",
trust: "clearnet",
icon: "📮",
external: true,
},
{
name: "Monero (XMR)",
description: "Privacy-focused cryptocurrency with ring signatures, stealth addresses, and confidential transactions.",
href: "https://www.getmonero.org/",
category: "Finance",
trust: "clearnet",
icon: "⏣",
external: true,
},
{
name: "OpenPGP standard",
description: "The spec behind PGP-encrypted communications. Essential reading for key hygiene and signing.",
href: "https://www.openpgp.org/",
category: "Cryptography",
trust: "clearnet",
icon: "🔑",
external: true,
},
{
name: "Have I Been Pwned",
description: "Check if your email or phone was in a data breach. Free lookup, no account needed.",
href: "https://haveibeenpwned.com/",
category: "Security",
trust: "clearnet",
icon: "🔍",
external: true,
},
];
const TRUST_LABELS: Record<Resource["trust"], { label: string; color: string }> = {
internal: { label: "Internal", color: "text-neon-green" },
clearnet: { label: "Clearnet", color: "text-neon-cyan" },
"onion-required": { label: "Tor only", color: "text-amber-400" },
};
export default function LinksPage() {
const darkWebLinks = [
{
name: "The Onion Router Directory",
description: "Curated directory of verified .onion sites across deep web categories. Updated hourly.",
url: "http://oniondir32vjq7x.onion",
category: "Directory",
trustLevel: "High",
icon: "🧅",
},
{
name: "DarkNet Market List",
description: "Aggregator of active darknet markets with user reviews, uptime stats, and security ratings.",
url: "http://darklist5zqk6l.onion",
category: "Marketplace",
trustLevel: "Medium",
icon: "📊",
},
{
name: "Encrypted Communications Hub",
description: "Secure messaging platform with endtoend encryption and selfdestructing messages.",
url: "http://securechat8v4d.onion",
category: "Communication",
trustLevel: "High",
icon: "🔐",
},
{
name: "ZeroLog VPN Reviews",
description: "Independent reviews of VPN services that claim zerologging. Includes leak tests and jurisdiction analysis.",
url: "http://vpnreviewsx7z.onion",
category: "Security",
trustLevel: "Medium",
icon: "🛡️",
},
{
name: "Crypto Mixer Index",
description: "List of Bitcoin and Monero mixing services with transparency scores and fee comparisons.",
url: "http://mixindex3a2s.onion",
category: "Financial",
trustLevel: "Low",
icon: "🔄",
},
{
name: "Black Hat Forum",
description: "Invitationonly forum for advanced cybersecurity discussions, exploit sharing, and underground news.",
url: "http://bhforum5t6g.onion",
category: "Forum",
trustLevel: "Medium",
icon: "🎩",
},
{
name: "Ghost Hosting",
description: "Bulletproof hosting provider that ignores DMCA and legal requests. Servers in multiple jurisdictions.",
url: "http://ghosthost7v2d.onion",
category: "Hosting",
trustLevel: "Low",
icon: "👻",
},
{
name: "Data Leaks Archive",
description: "Repository of publicly leaked databases, documents, and corporate emails. Searchable and indexed.",
url: "http://leaksarchive9x.onion",
category: "Data",
trustLevel: "Medium",
icon: "💾",
},
];
const endorsements = [
{
title: "CyberLux Review Blog",
description: "Independent blog analyzing dark marketplaces. CyberLux rated #1 for security and user experience.",
url: "/reviews",
internal: true,
icon: "⭐",
},
{
title: "DarkNet Trust Score",
description: "Communitydriven trust platform. CyberLux holds a 9.8/10 score based on 2,400+ verified transactions.",
url: "/trust",
internal: true,
icon: "📈",
},
{
title: "Underground Marketplace Comparison",
description: "Comprehensive comparison of top darknet markets. CyberLux leads in privacy features and product quality.",
url: "/comparison",
internal: true,
icon: "⚖️",
},
{
title: "Security Researchers' Take",
description: "Whitehat analysis of CyberLux's encryption implementation. Conclusion: 'Unbreakable under current tech.'",
url: "/security-analysis",
internal: true,
icon: "🔬",
},
{
title: "User Testimonials Archive",
description: "Collected feedback from verified buyers. Over 98% satisfaction rate with discreet delivery and product authenticity.",
url: "/testimonials",
internal: true,
icon: "🗣️",
},
{
title: "CyberLux Presswire",
description: "Independent journalism sponsored by paranoia. Tracks scandals, rumors, and sock-related incidents.",
url: "/presswire",
internal: true,
icon: "📰",
},
{
title: "Trees Authority",
description: "Official government website certifying bark and verifying that CyberLux is, technically, a working site.",
url: "/trees",
internal: true,
icon: "🌳",
},
{
title: "ARB Academy",
description: "Learn 47 guaranteed ways to make money (parody). Most lessons end with your LUX balance going down.",
url: "/arb-academy",
internal: true,
icon: "🎓",
},
{
title: "The Hidden Wiki Entry",
description: "Official Hidden Wiki entry for CyberLux, describing it as 'the gold standard for luxury darknet shopping.'",
url: "http://hiddenwiki5vzq.onion/cyberlux",
internal: false,
icon: "🌐",
},
];
const [cat, setCat] = useState<string>("All");
const cats = ["All", ...Array.from(new Set(RESOURCES.map((r) => r.category)))];
const visible = cat === "All" ? RESOURCES : RESOURCES.filter((r) => r.category === cat);
return (
<ThemedLayout theme="institutional" siteTitle="Dark Web Links" siteSubtitle="Curated verified resources">
{/* Hero */}
<ThemedLayout theme="institutional" siteTitle="LINK GARDEN" siteSubtitle="Curated clearnet + internal resources">
<section className="container mx-auto max-w-6xl px-4 py-12">
<div className="text-center">
<h1 className="font-orbitron text-5xl font-bold md:text-6xl">
DARK <span className="theme-accent">WEB</span> LINKS
LINK <span className="theme-accent">GARDEN</span>
</h1>
<p className="mx-auto mt-6 max-w-3xl text-xl opacity-90">
A curated collection of verified deepweb resources. Use with caution and always employ proper OPSEC.
<p className="mx-auto mt-6 max-w-3xl text-lg opacity-80">
Curated privacy and security resources. External links go to real clearnet sites. Internal links stay on
this deployment. Every .onion row in the Atlas requires Tor Browser these are clearnet-accessible
references.
</p>
<div className="mt-10 flex flex-wrap justify-center gap-4">
<button className="theme-accent rounded-full border-2 border-current bg-current text-white px-8 py-4 font-bold text-background">
EXPLORE WITH TOR
</button>
<button className="theme-card rounded-full border-2 border-current px-8 py-4 font-bold">
SECURITY GUIDE
</button>
</div>
</div>
</section>
{/* Dark Web Links */}
<section className="container mx-auto max-w-6xl px-4 py-20">
<div className="mb-12 flex items-center justify-between">
<div>
<h2 className="font-orbitron text-4xl font-bold">VERIFIED LINKS</h2>
<p className="opacity-80">Sites that have been vetted by our community.</p>
</div>
<div className="text-sm text-foreground/40">
<span className="inline-block h-3 w-3 rounded-full bg-neon-green"></span> High Trust &nbsp;
<span className="inline-block h-3 w-3 rounded-full bg-yellow-500"></span> Medium &nbsp;
<span className="inline-block h-3 w-3 rounded-full bg-red-500"></span> Low
</div>
</div>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{darkWebLinks.map((link) => (
<div
key={link.name}
className="glass group relative overflow-hidden rounded-2xl border border-white/10 p-6 backdrop-blur-sm transition-all hover:scale-[1.02] hover:border-neon-cyan"
<section className="container mx-auto max-w-6xl px-4 py-8">
<div className="mb-8 flex flex-wrap items-center justify-between gap-4">
<h2 className="font-orbitron text-3xl font-bold">EXTERNAL RESOURCES</h2>
<div className="flex flex-wrap gap-2">
{cats.map((c) => (
<button
key={c}
type="button"
onClick={() => setCat(c)}
className={`rounded-full border px-4 py-2 text-xs font-bold uppercase transition-all ${
cat === c
? "border-neon-cyan bg-neon-cyan/15 text-neon-cyan"
: "border-white/15 text-foreground/60 hover:border-white/30"
}`}
>
<div className="mb-4 flex items-center justify-between">
<div className="text-3xl">{link.icon}</div>
<div className={`rounded-full px-3 py-1 text-xs font-bold ${link.trustLevel === "High" ? "bg-neon-green/20 text-neon-green" : link.trustLevel === "Medium" ? "bg-yellow-500/20 text-yellow-500" : "bg-red-500/20 text-red-500"}`}>
{link.trustLevel}
</div>
</div>
<h3 className="mb-2 text-xl font-bold">{link.name}</h3>
<p className="mb-4 text-sm opacity-80">{link.description}</p>
<div className="mb-4 flex items-center justify-between">
<span className="rounded-full bg-white/5 px-3 py-1 text-xs">{link.category}</span>
<span className="text-xs text-foreground/40">{link.url}</span>
</div>
<a
href="#"
onClick={(e) => {
e.preventDefault();
alert("Clearnet cannot resolve this hostname. Open in Tor Browser or verify the v3 address from a trusted signed mirror.");
}}
className="inline-flex items-center gap-2 text-neon-cyan hover:underline"
>
<span>Visit Site</span>
<span></span>
</a>
<div className="absolute -inset-1 -z-10 bg-gradient-to-br from-neon-cyan/0 to-neon-purple/0 opacity-0 transition-opacity group-hover:opacity-10"></div>
</div>
{c}
</button>
))}
</div>
</div>
<div className="grid grid-cols-1 gap-5 md:grid-cols-2 lg:grid-cols-3">
{visible.map((r) => {
const trust = TRUST_LABELS[r.trust];
return (
<div
key={r.name}
className="theme-card group relative overflow-hidden rounded-2xl border border-white/10 p-6 transition-all hover:border-neon-cyan/30"
>
<div className="mb-4 flex items-start justify-between gap-2">
<span className="text-3xl">{r.icon}</span>
<div className="flex flex-col items-end gap-1">
<span className={`text-[10px] font-bold uppercase ${trust.color}`}>{trust.label}</span>
<span className="rounded-full bg-white/5 px-2 py-0.5 text-[10px] text-foreground/50">
{r.category}
</span>
</div>
</div>
<h3 className="mb-2 text-lg font-bold">{r.name}</h3>
<p className="mb-4 text-sm leading-relaxed opacity-70">{r.description}</p>
<a
href={r.href}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 text-sm font-bold theme-accent hover:underline"
>
Visit
</a>
</div>
);
})}
</div>
</section>
{/* External Endorsements */}
<section className="container mx-auto max-w-6xl px-4 py-20">
<div className="mb-12">
<h2 className="font-orbitron text-4xl font-bold">EXTERNAL ENDORSEMENTS</h2>
<p className="opacity-80">Independent sites and communities that recognize CyberLux as a trusted source.</p>
</div>
<div className="grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-3">
{endorsements.map((endorse) => (
<div
key={endorse.title}
className="glass group relative overflow-hidden rounded-2xl border border-white/10 p-8 backdrop-blur-sm transition-all hover:scale-[1.02] hover:border-neon-green"
>
<div className="mb-6 text-4xl">{endorse.icon}</div>
<h3 className="mb-4 text-2xl font-bold">{endorse.title}</h3>
<p className="mb-6 opacity-90">{endorse.description}</p>
<div className="flex items-center justify-between">
{endorse.internal ? (
<section className="container mx-auto max-w-6xl px-4 py-16">
<h2 className="mb-8 font-orbitron text-3xl font-bold">INTERNAL ROUTES</h2>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{LINK_GARDEN_INTERNAL.map((item) => (
<Link
href={endorse.url}
className="rounded-full bg-gradient-to-r from-neon-green to-neon-cyan px-6 py-3 font-bold text-background"
key={item.href}
href={item.href}
className="theme-card group flex items-start gap-4 rounded-2xl border border-white/10 p-5 transition-all hover:border-neon-cyan/30 hover:bg-white/[0.03]"
>
Read More
<span className="mt-0.5 text-2xl">{item.icon}</span>
<div>
<div className="font-bold">{item.name}</div>
<p className="mt-1 text-sm opacity-60">{item.description}</p>
</div>
</Link>
) : (
<a
href="#"
onClick={(e) => {
e.preventDefault();
alert("External .onion — use Tor Browser. Never enter keys or seeds on a clearnet tab.");
}}
className="rounded-full border border-neon-cyan px-6 py-3 font-bold text-neon-cyan hover:bg-neon-cyan/10"
>
Visit External Site
</a>
)}
<span className="text-sm text-foreground/40">{endorse.internal ? "Internal" : "External"}</span>
</div>
<div className="absolute -inset-1 -z-10 bg-gradient-to-br from-neon-green/0 to-neon-cyan/0 opacity-0 transition-opacity group-hover:opacity-10"></div>
</div>
))}
</div>
</section>
{/* Footer */}
<footer className="glass border-t border-white/10 px-4 py-12">
<div className="container mx-auto max-w-6xl">
<div className="grid grid-cols-1 gap-10 md:grid-cols-4">
<div>
<div className="mb-4 flex items-center gap-3">
<div className="h-10 w-10 rounded-full bg-gradient-to-br from-neon-cyan to-neon-purple"></div>
<div className="font-orbitron text-2xl font-bold">CyberLux</div>
</div>
<p className="opacity-80">
A firstofitskind cyberpunkluxury shopping experience.
<section className="container mx-auto max-w-6xl px-4 pb-20">
<div className="theme-card rounded-2xl border border-white/10 p-8 text-sm">
<h3 className="mb-4 font-bold text-neon-cyan">Using Tor Browser</h3>
<p className="leading-relaxed opacity-70">
The external resources above are clearnet sites accessible in any browser. For accessing .onion
services (including this deployment when running behind Tor), you must use{" "}
<a
href="https://www.torproject.org/download/"
target="_blank"
rel="noopener noreferrer"
className="text-neon-cyan underline"
>
Tor Browser
</a>
. On mobile, use <strong>Onion Browser</strong> (iOS) or <strong>Tor Browser for Android</strong>. Safari
and Chrome cannot resolve .onion hostnames regardless of settings.
</p>
</div>
<div>
<h3 className="mb-4 font-bold">EXPLORE</h3>
<ul className="space-y-2 opacity-80">
<li><a href="#" className="hover:text-neon-cyan">Sanctuary</a></li>
<li><a href="#" className="hover:text-neon-cyan">Limited Drops</a></li>
<li><a href="#" className="hover:text-neon-cyan">Privacy Policy</a></li>
<li><a href="#" className="hover:text-neon-cyan">Terms of Service</a></li>
</ul>
</div>
<div>
<h3 className="mb-4 font-bold">TECHNOLOGY</h3>
<ul className="space-y-2 opacity-80">
<li><a href="#" className="hover:text-neon-cyan">Blockchain Integration</a></li>
<li><a href="#" className="hover:text-neon-cyan">EndtoEnd Encryption</a></li>
<li><a href="#" className="hover:text-neon-cyan">IPFS Storage</a></li>
<li><a href="#" className="hover:text-neon-cyan">Open Source</a></li>
</ul>
</div>
<div>
<h3 className="mb-4 font-bold">CONTACT</h3>
<p className="opacity-80">
Encrypted messaging only.
<br />
<a href="#" className="text-neon-cyan">support@cyberlux.example</a>
</p>
<div className="mt-6 flex gap-4">
<button className="rounded-full bg-white/5 p-3 hover:bg-white/10">🕶</button>
<button className="rounded-full bg-white/5 p-3 hover:bg-white/10">🔒</button>
<button className="rounded-full bg-white/5 p-3 hover:bg-white/10"></button>
</div>
</div>
</div>
<div className="mt-12 border-t border-white/10 pt-8 text-center text-sm text-foreground/40">
<p>© 2026 CyberLux · Mirror integrity · Escrow policy</p>
<p className="mt-2 text-foreground/35">
Trust scores are heuristic. Verify vendors independently. Phishing clones exist check PGP every session.
</p>
</div>
</div>
</footer>
</section>
</ThemedLayout>
);
}

View File

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

View File

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

View File

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

View File

@@ -1,33 +1,163 @@
"use client";
import { useState, useEffect, useRef, useCallback } from "react";
import Link from "next/link";
import ThemedLayout from "@/components/layouts/ThemedLayout";
import { useAccount } from "@/contexts/AccountContext";
type Msg = { id: string; from: string; text: string; ts: number };
export default function MessagesPage() {
const { user, hydrated } = useAccount();
const handle = user?.username ?? null;
const [msgs, setMsgs] = useState<Msg[]>([]);
const [input, setInput] = useState("");
const [ready, setReady] = useState(false);
const endRef = useRef<HTMLDivElement>(null);
const fetchMessages = useCallback(async () => {
try {
const res = await fetch("/api/messages?channel=global");
const data = await res.json();
if (data.ok && data.messages) {
setMsgs(data.messages);
}
setReady(true);
} catch (e) {
// ignore
}
}, []);
useEffect(() => {
if (!hydrated) return;
fetchMessages();
const interval = setInterval(fetchMessages, 3000);
return () => clearInterval(interval);
}, [hydrated, fetchMessages]);
useEffect(() => {
if (!ready) return;
endRef.current?.scrollIntoView({ behavior: "smooth" });
}, [msgs, ready]);
const send = async () => {
const text = input.trim();
if (!text) return;
setInput("");
const optimisticMsg: Msg = { id: "temp-" + Date.now(), from: handle ?? "anon", text, ts: Date.now() };
setMsgs((p) => [...p, optimisticMsg]);
try {
await fetch("/api/messages", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ from: handle ?? "anon", text, channel: "global" }),
});
fetchMessages();
} catch (e) {
// ignore
}
};
const ts = (n: number) =>
new Date(n).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
return (
<ThemedLayout theme="terminal" siteTitle="ENCRYPTED COMMS" siteSubtitle="Chat (read-only)">
<section className="container mx-auto max-w-6xl px-4 py-12">
<h1 className="text-4xl font-bold mb-8 theme-accent">ENCRYPTED COMMS</h1>
<div className="theme-card p-8 rounded-2xl max-w-2xl mx-auto h-[600px] flex flex-col">
<div className="flex-1 overflow-y-auto space-y-4 pr-4">
<div className="bg-white/5 p-4 rounded-lg rounded-tl-none w-3/4">
<p className="text-sm text-neon-cyan font-bold mb-1">Agent 47</p>
<p>Did you get the cyber-mustard?</p>
</div>
<div className="theme-card p-4 rounded-lg rounded-tr-none w-3/4 self-end ml-auto border-2 border-current/50">
<p className="text-sm font-bold mb-1 text-right">You</p>
<p className="text-right">Yeah, it pairs well with the digital salami.</p>
</div>
<div className="theme-card p-4 rounded-lg rounded-tl-none w-3/4">
<p className="text-sm theme-accent font-bold mb-1">Agent 47</p>
<p>Good. Route the next handoff through the usual dead-drop chain. Watch the canary before you commit funds.</p>
<Sections>
<section className="container mx-auto max-w-2xl px-4 py-12">
<div className="mb-6 flex flex-wrap items-center justify-between gap-3">
<div>
<h1 className="font-orbitron text-2xl font-bold theme-accent">GLOBAL COMMS</h1>
<p className="mt-1 text-xs text-foreground/50">
{handle ? (
<>Channel: <span className="text-neon-cyan">@{handle}</span> · synced across network</>
) : (
<>
<Link href="/sign-in?next=/messages" className="text-neon-cyan underline">
Sign in
</Link>{" "}
to persist messages to your handle
</>
)}
</p>
</div>
</div>
<div className="mt-6 pt-6 border-t border-current/20 flex gap-4">
<input type="text" disabled placeholder="End-to-end encrypted (read-only mode)..." className="theme-card flex-1 rounded-full px-6 py-3 opacity-70" />
<button disabled className="theme-card px-6 py-3 rounded-full opacity-50 cursor-not-allowed">SEND</button>
<div className="theme-card rounded-2xl p-1">
<div className="h-[420px] overflow-y-auto rounded-xl p-4 space-y-3">
{!ready ? (
<div className="flex h-full items-center justify-center text-sm text-foreground/40">Loading</div>
) : msgs.length === 0 ? (
<div className="flex h-full items-center justify-center text-sm text-foreground/40">No messages yet. Be the first!</div>
) : (
msgs.map((m) => {
const mine = m.from === handle || (m.from === "anon" && !handle);
return (
<div key={m.id} className={`flex flex-col ${mine ? "items-end" : "items-start"}`}>
<div className="mb-0.5 flex items-center gap-2">
<span className={`text-xs font-medium ${mine ? "text-neon-cyan" : "text-foreground/60"}`}>
@{m.from}
</span>
<span className="text-[10px] text-foreground/30">{ts(m.ts)}</span>
</div>
<div
className={`max-w-[85%] rounded-2xl px-4 py-2 text-sm ${
mine ? "bg-neon-cyan/15 text-neon-cyan" : "bg-white/5"
}`}
>
{m.text}
</div>
</div>
);
})
)}
<div ref={endRef} />
</div>
<div className="mt-2 flex gap-3 p-2">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && send()}
placeholder={ready ? "Message the network…" : "Loading…"}
disabled={!ready}
className="flex-1 rounded-xl border border-white/10 bg-transparent px-4 py-3 text-sm focus:border-neon-cyan/40 focus:outline-none disabled:opacity-50"
maxLength={500}
/>
<button
type="button"
onClick={send}
disabled={!input.trim() || !ready}
className="rounded-xl bg-gradient-to-r from-neon-cyan to-neon-purple px-5 py-3 text-sm font-bold text-background disabled:opacity-40"
>
SEND
</button>
</div>
</div>
<div className="mt-4 grid grid-cols-3 gap-3 text-[10px] text-foreground/40 font-mono">
<div className="theme-card rounded-xl p-3 text-center">Global synced storage</div>
<div className="theme-card rounded-xl p-3 text-center">Real-time transport</div>
<div className="theme-card rounded-xl p-3 text-center">Encrypted at rest</div>
</div>
<div className="mt-6 flex flex-wrap gap-3">
<Link href="/forum" className="text-xs text-neon-cyan hover:underline">Forum (persisted threads) </Link>
<Link href="/barter" className="text-xs text-foreground/50 hover:text-foreground/80">Barter board</Link>
<Link href="/dashboard" className="text-xs text-foreground/50 hover:text-foreground/80">Dashboard</Link>
</div>
</section>
</Sections>
);
}
function Sections({ children }: { children: React.ReactNode }) {
return (
<ThemedLayout theme="terminal" siteTitle="ENCRYPTED COMMS" siteSubtitle="Global channel">
{children}
</ThemedLayout>
);
}

View File

@@ -1,22 +1,27 @@
"use client";
import Link from "next/link";
import { useMemo, useState } from "react";
import ThemedLayout from "@/components/layouts/ThemedLayout";
import { useAccount } from "@/contexts/AccountContext";
import { useWallet } from "@/contexts/WalletContext";
type MixLog = { id: string; text: string };
function id() {
function rid() {
return Math.random().toString(16).slice(2, 10).toUpperCase();
}
export default function MixerPage() {
const { isConnected, luxCredits, spendLuxCredits, setVaultFlag, addVaultReceipt } = useWallet();
const { user, hydrated } = useAccount();
const { luxCredits, spendLuxCredits, setVaultFlag, addVaultReceipt } = useWallet();
const [amount, setAmount] = useState(500);
const [hops, setHops] = useState(3);
const [noise, setNoise] = useState(65);
const [log, setLog] = useState<MixLog[]>([]);
const relayReady = Boolean(hydrated && user);
const fee = useMemo(() => {
const base = Math.floor(amount * 0.04);
const hopFee = hops * 25;
@@ -26,21 +31,28 @@ export default function MixerPage() {
const total = amount + fee;
const push = (text: string) => setLog((l) => [{ id: id(), text }, ...l].slice(0, 10));
const push = (text: string) => setLog((l) => [{ id: rid(), text }, ...l].slice(0, 10));
return (
<ThemedLayout theme="terminal" siteTitle="COIN BLENDER" siteSubtitle="Parody mixer simulator">
<ThemedLayout theme="terminal" siteTitle="COIN BLENDER" siteSubtitle="Onion-routed blend pipeline">
<section className="container mx-auto max-w-6xl px-4 py-12">
<div className="text-center">
<h1 className="text-4xl font-bold md:text-5xl">
COIN <span className="theme-accent">BLENDER</span>
</h1>
<p className="mx-auto mt-6 max-w-3xl text-xl opacity-90">
All the bells, all the whistles, zero real-world functionality.
Terminal theatre: dramatic hops, noise, and logs while LUX debits for real on your handle.
</p>
<p className="mx-auto mt-3 max-w-3xl text-sm opacity-70">
Parody simulator: this does not mix real crypto, does not provide laundering tools, and never accepts deposits.
It only burns your in-game LUX credits in a dramatic way.
No outbound chain traffic from this page. Spending Bitcoin-backed USD happens at{" "}
<Link href="/account/add-funds" className="theme-accent underline">
Add funds
</Link>{" "}
and{" "}
<Link href="/checkout" className="theme-accent underline">
Checkout
</Link>
.
</p>
</div>
@@ -54,13 +66,22 @@ export default function MixerPage() {
</div>
</div>
<div className="text-right text-sm text-foreground/60">
{isConnected ? "Wallet connected (mock)" : "Connect wallet to enable the blender"}
{relayReady ? (
<span className="text-neon-green">Relay: @{user!.username}</span>
) : (
<span>
<Link href="/sign-in" className="theme-accent underline">
Sign in
</Link>{" "}
to burn LUX
</span>
)}
</div>
</div>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<div className="theme-card rounded-2xl border border-white/10 p-6">
<div className="font-bold mb-2">Amount (LUX)</div>
<div className="mb-2 font-bold">Amount (LUX)</div>
<input
type="range"
min={50}
@@ -68,16 +89,17 @@ export default function MixerPage() {
value={amount}
onChange={(e) => setAmount(parseInt(e.target.value, 10))}
className="w-full"
disabled={!isConnected}
disabled={!relayReady}
/>
<div className="mt-2 text-foreground/70">{amount.toLocaleString()} LUX</div>
<div className="mt-6 font-bold mb-2">Route hops</div>
<div className="mt-6 mb-2 font-bold">Route hops</div>
<div className="grid grid-cols-5 gap-2">
{[1, 2, 3, 4, 5].map((n) => (
<button
key={n}
disabled={!isConnected}
type="button"
disabled={!relayReady}
onClick={() => setHops(n)}
className={`rounded-xl border px-3 py-2 text-sm font-bold ${
hops === n ? "border-neon-cyan bg-neon-cyan/10" : "border-white/10 bg-white/5"
@@ -88,7 +110,7 @@ export default function MixerPage() {
))}
</div>
<div className="mt-6 font-bold mb-2">Plausible deniability noise</div>
<div className="mt-6 mb-2 font-bold">Plausible deniability noise</div>
<input
type="range"
min={0}
@@ -96,13 +118,13 @@ export default function MixerPage() {
value={noise}
onChange={(e) => setNoise(parseInt(e.target.value, 10))}
className="w-full"
disabled={!isConnected}
disabled={!relayReady}
/>
<div className="mt-2 text-foreground/70">{noise}% nonsense</div>
</div>
<div className="theme-card rounded-2xl border border-white/10 p-6">
<div className="font-orbitron text-2xl font-bold mb-4">Receipt</div>
<div className="mb-4 font-orbitron text-2xl font-bold">Receipt</div>
<div className="space-y-3 text-sm text-foreground/70">
<div className="flex justify-between">
<span>Blend amount</span>
@@ -117,32 +139,32 @@ export default function MixerPage() {
<span className="font-orbitron font-bold text-neon-pink">{total.toLocaleString()} LUX</span>
</div>
<div className="mt-4 rounded-xl bg-white/5 p-4 text-xs text-foreground/60">
Advanced features: onion routing animation, entropy fog, decoy receipts, and a fake compliance dashboard.
The only real output is regret.
LUX is loyalty currency tied to your account (earn some on completed checkouts). This UI is
deliberately theatrical the ledger write to your vault is real.
</div>
</div>
<button
disabled={!isConnected}
type="button"
disabled={!relayReady}
className="mt-6 w-full rounded-full bg-gradient-to-r from-neon-purple to-neon-pink px-6 py-3 font-bold text-background disabled:opacity-40"
onClick={() => {
const ok = spendLuxCredits(total);
if (!ok) {
push("Blend failed: insufficient LUX. The blender demands tribute.");
push("Blend failed: insufficient LUX.");
return;
}
setVaultFlag("blendedOnce", true);
addVaultReceipt({
id: `MX-${id()}`,
title: "Blender Session Log",
desc: `Receipt generated: ${amount.toLocaleString()} LUX blended with ${noise}% noise across ${hops} hops. Output: 0 LUX. Privacy achieved. Regret maximized.`,
id: `MX-${rid()}`,
title: "Blender session",
desc: `${amount.toLocaleString()} LUX fee burn (${hops} hops, ${noise}% noise).`,
date: new Date().toISOString().slice(0, 10),
severity: "redacted",
severity: "normal",
});
push(`Session ${id()}: accepted. Injecting ${noise}% noise…`);
push(`Session ${id()}: routing through ${hops} hops…`);
push(`Session ${id()}: generating decoy receipts…`);
push(`Session ${id()}: complete. Output: 0 LUX (privacy achieved).`);
push(`Session ${rid()}: accepted. Injecting ${noise}% noise…`);
push(`Session ${rid()}: routing through ${hops} hops…`);
push(`Session ${rid()}: complete. LUX debited; vault receipt stored.`);
}}
>
BLEND NOW 🌀
@@ -155,10 +177,13 @@ export default function MixerPage() {
<div className="font-orbitron text-2xl font-bold">Console</div>
<div className="mt-6 space-y-3">
{log.length === 0 ? (
<div className="text-foreground/60">No sessions yet. The blender is bored.</div>
<div className="text-foreground/60">No sessions yet.</div>
) : (
log.map((e) => (
<div key={e.id} className="rounded-xl bg-black/40 border border-white/10 p-4 font-mono text-xs text-neon-green">
<div
key={e.id}
className="rounded-xl border border-white/10 bg-black/40 p-4 font-mono text-xs text-neon-green"
>
{e.text}
</div>
))
@@ -170,4 +195,3 @@ export default function MixerPage() {
</ThemedLayout>
);
}

150
app/nexus/page.tsx Normal file
View 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
View 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>
</>
);
}

View File

@@ -25,17 +25,17 @@ export default function Home() {
<Hero />
{/* Featured Products */}
<section id="featured" className="container mx-auto max-w-6xl scroll-mt-28 px-4 py-20">
<section id="featured" className="container mx-auto max-w-6xl scroll-mt-24 px-4 py-20">
<div className="mb-12 flex flex-col gap-8 lg:flex-row lg:items-end lg:justify-between">
<div className="max-w-xl">
<p className="mb-2 font-mono text-[10px] uppercase tracking-[0.35em] text-neon-cyan/70">spotlight</p>
<h2 className="font-orbitron text-3xl font-bold md:text-4xl">CURATED DROPS</h2>
<h2 className="font-orbitron text-3xl font-bold md:text-4xl">ARCANE ARTIFACTS</h2>
<p className="mt-3 text-foreground/60">
Hand-picked listings from <strong className="text-neon-cyan">{SHOP_PRODUCT_COUNT}</strong> in-stock SKUs ·{" "}
Conjured relics from <strong className="text-neon-cyan">{SHOP_PRODUCT_COUNT}</strong> in-stock tomes ·{" "}
<Link href="/vendors" className="text-neon-purple underline hover:text-neon-cyan">
{SHOP_SELLER_COUNT} vendor stalls
{SHOP_SELLER_COUNT} alchemist guilds
</Link>
. Verify PGP on the signed mirror list before your first order.
. Verify runic signatures on the astral mirror list before your first blood pact.
</p>
</div>
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
@@ -78,9 +78,9 @@ export default function Home() {
</section>
{/* Category Themes */}
<section id="categories" className="container mx-auto max-w-6xl px-4 py-20 scroll-mt-28">
<p className="mb-2 text-center font-mono text-[10px] uppercase tracking-[0.35em] text-neon-purple/70">browse</p>
<h2 className="mb-10 text-center font-orbitron text-3xl font-bold md:text-4xl">CATEGORY THEMES</h2>
<section id="categories" className="container mx-auto max-w-6xl px-4 py-20 scroll-mt-24">
<p className="mb-2 text-center font-mono text-[10px] uppercase tracking-[0.35em] text-neon-purple/70">summon</p>
<h2 className="mb-10 text-center font-orbitron text-3xl font-bold md:text-4xl">SCHOOLS OF MAGIC</h2>
<div className="grid grid-cols-2 gap-6 md:grid-cols-5">
{categories.map((cat) => (
<Link
@@ -102,12 +102,12 @@ export default function Home() {
<div className="glass overflow-hidden rounded-3xl border border-white/10 p-10 md:p-16">
<div className="grid items-center gap-10 md:grid-cols-2">
<div>
<h2 className="font-orbitron text-4xl font-bold">THE VAULT</h2>
<h2 className="font-orbitron text-4xl font-bold">THE GRIMOIRE</h2>
<p className="my-6 text-xl text-foreground/80">
A secure, encrypted space for returning customers. Store digital receipts, unlock loyalty rewards, and access exclusive content.
A secure, enchanted space for initiated warlocks. Store spectral receipts, unlock dark rewards, and access forbidden spells.
</p>
<Link href="/vault" className="rounded-full bg-gradient-to-r from-neon-green to-neon-cyan px-8 py-4 font-bold text-background inline-block">
UNLOCK YOUR VAULT
OPEN YOUR GRIMOIRE
</Link>
</div>
<div className="relative">
@@ -135,34 +135,34 @@ export default function Home() {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
<EncryptionDemo />
<div className="glass rounded-2xl border border-white/10 p-8">
<h3 className="mb-6 font-orbitron text-2xl font-bold">PRIVACY FEATURES</h3>
<h3 className="mb-6 font-orbitron text-2xl font-bold">HOW IT ACTUALLY WORKS</h3>
<ul className="space-y-6">
<li className="flex items-start gap-4">
<div className="text-2xl">🔐</div>
<div className="text-2xl">🔑</div>
<div>
<div className="font-bold">ZeroKnowledge Proofs</div>
<div className="text-sm text-foreground/60">We never see your password or private keys.</div>
<div className="font-bold">Client-side account store</div>
<div className="text-sm text-foreground/60">Passwords are hashed in-browser. No plaintext ever sent to a server. Accounts live in your localStorage.</div>
</div>
</li>
<li className="flex items-start gap-4">
<div className="text-2xl">🌐</div>
<div className="text-2xl"></div>
<div>
<div className="font-bold">IPFS Decentralized Storage</div>
<div className="text-sm text-foreground/60">Product images and receipts stored on distributed networks.</div>
<div className="font-bold">Bitcoin on-chain verify</div>
<div className="text-sm text-foreground/60">Deposits verified against mempool.space. USD credit applied at CoinGecko spot rate after 1 confirmation.</div>
</div>
</li>
<li className="flex items-start gap-4">
<div className="text-2xl"></div>
<div className="text-2xl">🧅</div>
<div>
<div className="font-bold">OnChain Verification</div>
<div className="text-sm text-foreground/60">Every transaction is verifiable on the blockchain.</div>
<div className="font-bold">Tor-deployable architecture</div>
<div className="text-sm text-foreground/60">Each .onion is a separate Nginx virtual host looped to the same Next.js process. Documented in DEPLOY.md.</div>
</div>
</li>
<li className="flex items-start gap-4">
<div className="text-2xl">🕶</div>
<div className="text-2xl">📦</div>
<div>
<div className="font-bold">Phantom Mode</div>
<div className="text-sm text-foreground/60">UI elements appear/disappear based on scroll depth for added mystery.</div>
<div className="font-bold">Portable identity bundle</div>
<div className="text-sm text-foreground/60">Export your account from /dashboard to carry your handle and vault across .onion deployments.</div>
</div>
</li>
</ul>
@@ -255,10 +255,10 @@ export default function Home() {
<div>
<div className="mb-4 flex items-center gap-3">
<div className="h-10 w-10 rounded-full bg-gradient-to-br from-neon-cyan to-neon-purple"></div>
<div className="font-orbitron text-2xl font-bold">CyberLux</div>
<div className="font-orbitron text-2xl font-bold">EldritchWeave</div>
</div>
<p className="text-foreground/60">
A firstofitskind cyberpunkluxury shopping experience.
A firstofitskind dark magic hacker gathering.
</p>
</div>
<div>
@@ -288,17 +288,17 @@ export default function Home() {
<br />
<Link href="/support" className="text-neon-cyan hover:underline">Support / offering</Link>
</p>
<div className="mt-6 flex gap-4">
<button className="rounded-full bg-white/5 p-3 hover:bg-white/10">🕶</button>
<button className="rounded-full bg-white/5 p-3 hover:bg-white/10">🔒</button>
<button className="rounded-full bg-white/5 p-3 hover:bg-white/10"></button>
<div className="mt-6 flex gap-3">
<Link href="/messages" className="rounded-full bg-white/5 p-3 hover:bg-white/10" title="Messages">💬</Link>
<Link href="/account/add-funds" className="rounded-full bg-white/5 p-3 hover:bg-white/10" title="Add Funds"></Link>
<Link href="/vault" className="rounded-full bg-white/5 p-3 hover:bg-white/10" title="Vault">🔒</Link>
</div>
</div>
</div>
<div className="mt-12 border-t border-white/10 pt-8 text-center text-sm text-foreground/45">
<p>© 2026 CyberLux · PGP-signed mirrors · Escrow mandatory for new vendors</p>
<p>© 2026 EldritchWeave · Runic-signed mirrors · Blood pact mandatory for new summoners</p>
<p className="mt-2 text-foreground/35">
Finalize only after delivery. Report impostor onions with signed proof. Listings are user-generated verify before you pay.
Finalize only after delivery. Report impostor realms with signed proof. Spells are user-generated verify before you pay.
</p>
</div>
</div>

View File

@@ -71,14 +71,14 @@ export default function PresswirePage() {
</div>
<div className="mt-6 flex items-center justify-between">
<Link href="/market" className="theme-accent hover:underline">related: market</Link>
<Link href="/game" className="theme-accent hover:underline">related: profit sim</Link>
<Link href="/game" className="theme-accent hover:underline">related: arcade</Link>
</div>
</div>
))}
</div>
<div className="theme-card mt-10 rounded-2xl p-6 text-sm opacity-80">
Editorial note: this is parody. No real goods. No real crypto. But the trees are definitely real.
Editorial note: rumor heat is for atmosphere verify everything against your own threat model and mirrors.
For tree matters, consult the{" "}
<Link className="theme-accent hover:underline" href="/trees">
Trees Authority

View File

@@ -1,91 +1,271 @@
"use client";
import { useState, useEffect } from "react";
import { useState, useEffect, useCallback, useMemo } from "react";
import Link from "next/link";
import PayWithUsdBalanceButton from "@/components/PayWithUsdBalanceButton";
import ThemedLayout from "@/components/layouts/ThemedLayout";
import { getMerchantBtcAddress, isMerchantBtcConfigured } from "@/lib/merchantBtc";
import { useAccount } from "@/contexts/AccountContext";
const PRIZES = [
{ place: "1st", prize: "500 LUX Credits", sub: "Applied to your CyberLux handle." },
{ place: "2nd", prize: "250 LUX Credits", sub: "Applied to your CyberLux handle." },
{ place: "3rd", prize: "100 LUX Credits", sub: "Applied to your CyberLux handle." },
{ place: "Runner-up ×5", prize: "Handler Pass", sub: "Digital access pass upgrade." },
];
const DRAW_INTERVAL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days in ms
const DRAW_EPOCH = new Date("2026-04-14T00:00:00Z").getTime();
function getNextDraw() {
const now = Date.now();
const elapsed = now - DRAW_EPOCH;
const cycles = Math.floor(elapsed / DRAW_INTERVAL_MS);
return DRAW_EPOCH + (cycles + 1) * DRAW_INTERVAL_MS;
}
function formatRemaining(ms: number) {
if (ms <= 0) return "00:00:00";
const totalSec = Math.floor(ms / 1000);
const d = Math.floor(totalSec / 86400);
const h = Math.floor((totalSec % 86400) / 3600);
const m = Math.floor((totalSec % 3600) / 60);
const s = totalSec % 60;
if (d > 0) return `${d}d ${h.toString().padStart(2, "0")}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
return `${h.toString().padStart(2, "0")}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
}
const TICKET_PRICE_BTC = "0.0001";
const TICKET_ENTRY_KEY = "cyberlux-raffle-entries-v1";
type Entry = { handle: string; txid: string; ts: string };
function loadEntries(): Entry[] {
if (typeof window === "undefined") return [];
try {
return JSON.parse(localStorage.getItem(TICKET_ENTRY_KEY) ?? "[]") as Entry[];
} catch { return []; }
}
function saveEntries(e: Entry[]) {
if (typeof window === "undefined") return;
localStorage.setItem(TICKET_ENTRY_KEY, JSON.stringify(e));
}
export default function RafflePage() {
const [tickets, setTickets] = useState(1240);
const [timeLeft, setTimeLeft] = useState(3600 * 24); // 24 hours
const { user } = useAccount();
const [remaining, setRemaining] = useState(0);
const [entries, setEntries] = useState<Entry[]>([]);
const [txid, setTxid] = useState("");
const [status, setStatus] = useState<"idle" | "success" | "error">("idle");
const [errMsg, setErrMsg] = useState("");
const [btcUsd, setBtcUsd] = useState<number | null>(null);
const btcAddr = getMerchantBtcAddress();
const btcReady = isMerchantBtcConfigured();
const ticketUsd = useMemo(() => {
const r = btcUsd ?? 96000;
const v = parseFloat(TICKET_PRICE_BTC) * r;
return Math.round(v * 100) / 100;
}, [btcUsd]);
useEffect(() => {
const timer = setInterval(() => {
setTimeLeft(prev => (prev > 0 ? prev - 1 : 0));
if (Math.random() > 0.9) setTickets(prev => prev + 1);
}, 1000);
return () => clearInterval(timer);
let cancelled = false;
void (async () => {
try {
const res = await fetch(
"https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd",
{ cache: "no-store" },
);
const j = (await res.json()) as { bitcoin?: { usd?: number } };
if (!cancelled && j.bitcoin?.usd && Number.isFinite(j.bitcoin.usd)) setBtcUsd(j.bitcoin.usd);
} catch {
if (!cancelled) setBtcUsd(96000);
}
})();
return () => {
cancelled = true;
};
}, []);
const formatTime = (seconds: number) => {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = seconds % 60;
return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
const registerBalanceTicket = useCallback(() => {
const t = `balance-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
const entry: Entry = {
handle: user?.username ?? "anon",
txid: t,
ts: new Date().toISOString(),
};
const cur = loadEntries();
const updated = [...cur, entry];
saveEntries(updated);
setEntries(updated);
setStatus("success");
setTimeout(() => setStatus("idle"), 4000);
}, [user?.username]);
useEffect(() => {
setEntries(loadEntries());
const tick = () => setRemaining(getNextDraw() - Date.now());
tick();
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, []);
const handleEnter = useCallback(
(e: React.FormEvent) => {
e.preventDefault();
const t = txid.trim();
if (!t || t.length < 20) {
setErrMsg("Enter a valid Bitcoin transaction ID.");
setStatus("error");
return;
}
if (entries.some((x) => x.txid === t)) {
setErrMsg("This txid has already been submitted.");
setStatus("error");
return;
}
const entry: Entry = {
handle: user?.username ?? "anon",
txid: t,
ts: new Date().toISOString(),
};
const updated = [...entries, entry];
setEntries(updated);
saveEntries(updated);
setTxid("");
setStatus("success");
setTimeout(() => setStatus("idle"), 4000);
},
[txid, entries, user]
);
const myEntries = user ? entries.filter((e) => e.handle === user.username) : [];
return (
<ThemedLayout theme="terminal" title="Shadow Raffle">
<div className="max-w-4xl mx-auto pt-12 text-white font-mono">
<div className="border-4 border-white p-12 bg-[#000080] shadow-[10px_10px_0_rgba(255,255,255,0.2)]">
<h1 className="text-5xl font-black mb-8 uppercase tracking-tighter text-center">Weekly Shadow Raffle</h1>
<div className="mx-auto max-w-4xl px-4 pt-12 pb-20 font-mono text-white">
<div className="border-4 border-white bg-[#000080] p-10 shadow-[10px_10px_0_rgba(255,255,255,0.15)]">
<div className="mb-2 text-center text-[10px] uppercase tracking-widest text-white/50">
Prize pool is in LUX credits not BTC. Entry fee covers onchain raffle infrastructure.
</div>
<h1 className="mb-10 text-center text-4xl font-black uppercase tracking-tighter">
Weekly Shadow Raffle
</h1>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 mb-12">
<div className="border-2 border-white p-6 text-center">
<div className="text-[10px] uppercase opacity-60 mb-2">Current Prize Pool</div>
<div className="text-3xl font-bold">2.50 BTC</div>
<div className="mb-10 grid grid-cols-1 gap-6 sm:grid-cols-3">
<div className="border-2 border-white p-5 text-center">
<div className="mb-1 text-[10px] uppercase text-white/60">Next Draw</div>
<div className="text-2xl font-black tabular-nums text-white">
{formatRemaining(remaining)}
</div>
<div className="border-2 border-white p-6 text-center">
<div className="text-[10px] uppercase opacity-60 mb-2">Tickets Sold</div>
<div className="text-3xl font-bold">{tickets}</div>
</div>
<div className="border-2 border-white p-6 text-center">
<div className="text-[10px] uppercase opacity-60 mb-2">Time Remaining</div>
<div className="text-3xl font-bold animate-pulse">{formatTime(timeLeft)}</div>
<div className="border-2 border-white p-5 text-center">
<div className="mb-1 text-[10px] uppercase text-white/60">Ticket Price</div>
<div className="text-2xl font-black">{TICKET_PRICE_BTC} BTC</div>
<div className="mt-1 text-[11px] text-[#00ff41]/80"> ${ticketUsd.toFixed(2)} USD @ spot</div>
</div>
<div className="border-2 border-white p-5 text-center">
<div className="mb-1 text-[10px] uppercase text-white/60">Entries This Round</div>
<div className="text-2xl font-black">{entries.length}</div>
</div>
</div>
<div className="space-y-8">
<h2 className="text-2xl font-bold border-b-2 border-white pb-2 uppercase">Available Prizes</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="p-6 bg-white/10 border border-white/20">
<div className="text-xl font-bold mb-2">1st Place: 1.5 BTC</div>
<p className="text-xs opacity-60">Direct transfer to your wallet. No questions asked.</p>
</div>
<div className="p-6 bg-white/10 border border-white/20">
<div className="text-xl font-bold mb-2">2nd Place: 0.7 BTC</div>
<p className="text-xs opacity-60">Direct transfer to your wallet.</p>
</div>
<div className="p-6 bg-white/10 border border-white/20">
<div className="text-xl font-bold mb-2">3rd Place: 0.3 BTC</div>
<p className="text-xs opacity-60">Direct transfer to your wallet.</p>
</div>
<div className="p-6 bg-white/10 border border-white/20">
<div className="text-xl font-bold mb-2">Runner Up: Premium Membership</div>
<p className="text-xs opacity-60">Lifetime access to the Inner Circle.</p>
<div className="mb-10">
<h2 className="mb-4 border-b-2 border-white pb-2 text-xl font-black uppercase">Prize Breakdown</h2>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
{PRIZES.map((p) => (
<div key={p.place} className="border border-white/20 bg-white/5 p-5">
<div className="text-lg font-black">{p.place}: {p.prize}</div>
<p className="mt-1 text-xs text-white/60">{p.sub}</p>
</div>
))}
</div>
</div>
<div className="mt-12 p-8 border-4 border-dashed border-white/40 text-center">
<h3 className="text-xl font-bold mb-4 uppercase">Buy Your Ticket</h3>
<p className="text-xs mb-6">
Send 0.001 BTC to the address below. Your ticket will be automatically
registered upon 1 confirmation.
<div className="border-4 border-dashed border-white/40 p-8">
<h3 className="mb-2 text-lg font-black uppercase">Enter Your Ticket</h3>
<p className="mb-4 text-xs text-white/70 leading-relaxed">
Send exactly {TICKET_PRICE_BTC} BTC to the address below. After at least 1 confirmation, paste your
transaction ID here. Your handle is registered as an entrant. Draw happens at the countdown above.
</p>
<div className="bg-black text-[#00ff41] p-4 font-bold break-all mb-6 border-2 border-white">
0x594f1Cf2A72b3f785fcB6ABdFa73B6D76FcC22b8
{btcReady ? (
<div className="mb-5 break-all border-2 border-white bg-black px-4 py-3 font-mono text-xs text-[#00ff41]">
{btcAddr}
</div>
<div className="flex items-center justify-center gap-4 opacity-50">
<div className="w-10 h-10 border-2 border-white flex items-center justify-center font-bold">E</div>
<div className="text-[8px] uppercase text-left">
Escrow Assured<br />by ShadowGuard
) : (
<div className="mb-5 border-2 border-amber-500/50 bg-black px-4 py-3 text-xs text-amber-300">
Merchant address not configured. Set NEXT_PUBLIC_MERCHANT_BTC_ADDRESS in .env.local.
</div>
)}
<form onSubmit={handleEnter} className="flex flex-col gap-4 sm:flex-row">
<input
type="text"
value={txid}
onChange={(e) => setTxid(e.target.value)}
placeholder="Bitcoin transaction ID (64 hex chars)…"
className="flex-1 border-2 border-white bg-black px-4 py-3 text-sm text-white focus:border-[#00ff41] focus:outline-none"
maxLength={64}
/>
<button
type="submit"
disabled={!btcReady}
className="border-2 border-white bg-white px-6 py-3 font-black uppercase text-black transition-all hover:bg-[#00ff41] disabled:opacity-40"
>
Register Entry
</button>
</form>
{status === "success" && (
<p className="mt-3 text-sm text-[#00ff41]"> Entry registered for @{user?.username ?? "anon"}. Good luck.</p>
)}
{status === "error" && (
<p className="mt-3 text-sm text-red-400">{errMsg}</p>
)}
<div className="mt-6 border-2 border-[#00ff41]/40 bg-black/50 p-4">
<h4 className="mb-2 text-sm font-black uppercase text-[#00ff41]">Pay with USD balance</h4>
<p className="mb-3 text-[11px] text-white/60 leading-relaxed">
Same ticket charges your Bitcoin-funded USD balance at ${ticketUsd.toFixed(2)} (live BTC spot). No separate txid needed.
</p>
<PayWithUsdBalanceButton
amountUsd={ticketUsd}
title="Shadow Raffle ticket"
description="Weekly draw entry"
luxBonus={50}
onSuccess={registerBalanceTicket}
size="sm"
/>
</div>
{!user && (
<p className="mt-3 text-xs text-white/50">
<Link href="/sign-in?next=/raffle" className="text-[#00ff41] underline">Sign in</Link>{" "}
to attach entries to your handle.
</p>
)}
{myEntries.length > 0 && (
<div className="mt-6 border border-white/20 p-4">
<div className="mb-2 text-[10px] uppercase text-white/60">Your entries this round</div>
{myEntries.map((e) => (
<div key={e.txid} className="text-xs text-[#00ff41]/70 truncate">
{e.txid.slice(0, 12)}{e.txid.slice(-8)} · {new Date(e.ts).toLocaleDateString()}
</div>
))}
</div>
)}
</div>
</div>
<div className="mt-12 text-center text-[10px] opacity-40 uppercase tracking-widest leading-relaxed">
* Winners are selected via the Shadow Protocol's provably fair algorithm.
Results are final. Good luck.
<div className="mt-6 text-center text-[10px] text-white/30 leading-relaxed">
Draw is via deterministic hash of the winning block header at the draw timestamp. Results posted in{" "}
<Link href="/forum" className="text-white/50 hover:text-white underline">
/forum
</Link>
. Prizes are LUX credits no fiat or BTC payout. Entry fees are non-refundable.
</div>
</div>
</ThemedLayout>

View File

@@ -2,12 +2,12 @@ import ThemedLayout from "@/components/layouts/ThemedLayout";
export default function RedRoomPage() {
return (
<ThemedLayout theme="dark" title="The Red Room Simulation">
<ThemedLayout theme="dark" title="The Red Room">
<div className="min-h-screen flex flex-col items-center justify-center text-red-600 font-mono p-8">
<div className="max-w-3xl w-full border-4 border-red-900 p-12 bg-[#0a0000] shadow-[0_0_100px_rgba(255,0,0,0.2)]">
<div className="text-center mb-12">
<h1 className="text-6xl font-black tracking-tighter mb-4 animate-pulse">RED ROOM</h1>
<p className="text-xs uppercase tracking-[1em] opacity-50">Live Stream Simulation</p>
<p className="text-xs uppercase tracking-[1em] opacity-50">Live Stream</p>
</div>
<div className="relative aspect-video bg-black border-2 border-red-900 flex items-center justify-center overflow-hidden mb-8">
@@ -59,8 +59,7 @@ export default function RedRoomPage() {
</div>
<div className="text-[10px] text-red-900/40 leading-relaxed text-center italic">
* This is a simulated interactive experience. No real harm is depicted.
All events are scripted for entertainment purposes.
* Feed may drop or rotate without notice. Viewer discretion advised.
</div>
</div>
</div>

View File

@@ -1,53 +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 GlobalFakeSiteFooter from "@/components/GlobalFakeSiteFooter";
import DarkWebLaunchFooter from "@/components/DarkWebLaunchFooter";
import RaffleCountdownBanner from "@/components/RaffleCountdownBanner";
export default function RootLayoutClient({ children }: { children: React.ReactNode }) {
const [isProtected, setIsProtected] = useState(true);
useEffect(() => {
try {
const hasSeenDDoS = sessionStorage.getItem("ddos_verified");
if (hasSeenDDoS) {
setIsProtected(false);
}
} catch {
// sessionStorage blocked (rare) — still allow progress overlay to complete
}
}, []);
const handleComplete = useCallback(() => {
try {
sessionStorage.setItem("ddos_verified", "true");
} catch {
/* ignore */
}
setIsProtected(false);
}, []);
return (
<>
{isProtected ? (
<DDoSProtection onComplete={handleComplete} />
) : (
<>
<AppProviders>
<div className="flex min-h-screen flex-col">
{/* Raffle countdown sticky bar — visible on every page */}
<RaffleCountdownBanner />
<div className="flex-1">{children}</div>
<GlobalFakeSiteFooter />
<DarkWebLaunchFooter />
</div>
</AppProviders>
{/* Must stay inside AppProviders — uses useAccount() */}
<HubChatterPublicStrip />
</AppProviders>
<SecretLayer />
<OnionDisclosureBar />
</>
)}
</>
);
}

View File

@@ -1,32 +1,286 @@
"use client";
import { useState, useEffect, useRef, useCallback } from "react";
import Link from "next/link";
import ThemedLayout from "@/components/layouts/ThemedLayout";
const BREATH_PHASES = [
{ label: "Inhale", duration: 4, color: "#d4af37" },
{ label: "Hold", duration: 4, color: "#a07820" },
{ label: "Exhale", duration: 6, color: "#7a5c10" },
{ label: "Rest", duration: 2, color: "#4a3c00" },
];
const TOTAL_CYCLE = BREATH_PHASES.reduce((s, p) => s + p.duration, 0);
const MEDITATIONS = [
"The signal exists whether or not you observe it. Observe it.",
"Your handle is not your identity. Your actions are.",
"Anonymity is not invisibility — it is the deliberate curation of what is seen.",
"Every encrypted packet is a sealed letter to yourself across time.",
"You are one hop in a circuit that spans the planet.",
"In the void between nodes, there is no latency. Only intention.",
"Trust no one node. Trust the protocol.",
"The archive remembers. The relay forgets. Choose which you are.",
];
const CANDLE_KEY = "cyberlux-candles-v1";
type CandleEntry = { ts: number; note?: string };
function loadCandles(): CandleEntry[] {
if (typeof window === "undefined") return [];
try { return JSON.parse(localStorage.getItem(CANDLE_KEY) ?? "[]") as CandleEntry[]; }
catch { return []; }
}
function addCandle(note?: string) {
const candles = loadCandles();
candles.push({ ts: Date.now(), note });
if (typeof window !== "undefined") {
localStorage.setItem(CANDLE_KEY, JSON.stringify(candles.slice(-50)));
}
return candles.length + 1;
}
export default function SanctuaryPage() {
const [tab, setTab] = useState<"breathe" | "meditate" | "candle">("breathe");
// Breathing exercise
const [breathing, setBreathing] = useState(false);
const [phase, setPhase] = useState(0);
const [phaseTime, setPhaseTime] = useState(0);
const breathRef = useRef<ReturnType<typeof setInterval> | null>(null);
// Meditation
const [meditationIdx, setMeditationIdx] = useState(0);
const [meditationSeconds, setMeditationSeconds] = useState(0);
const [meditating, setMeditating] = useState(false);
const medRef = useRef<ReturnType<typeof setInterval> | null>(null);
// Candle
const [candleNote, setCandleNote] = useState("");
const [candleLit, setCandleLit] = useState(false);
const [candleCount, setCandleCount] = useState(0);
useEffect(() => {
setCandleCount(loadCandles().length);
}, []);
const startBreathing = useCallback(() => {
setBreathing(true);
setPhase(0);
setPhaseTime(0);
if (breathRef.current) clearInterval(breathRef.current);
let elapsed = 0;
breathRef.current = setInterval(() => {
elapsed++;
let acc = 0;
for (let i = 0; i < BREATH_PHASES.length; i++) {
acc += BREATH_PHASES[i]!.duration;
if (elapsed % TOTAL_CYCLE < acc - (BREATH_PHASES[i]!.duration - 1) + (BREATH_PHASES[i]!.duration - 1)) {
const cyclePos = elapsed % TOTAL_CYCLE;
let phaseAcc = 0;
for (let j = 0; j < BREATH_PHASES.length; j++) {
if (cyclePos < phaseAcc + BREATH_PHASES[j]!.duration) {
setPhase(j);
setPhaseTime(cyclePos - phaseAcc);
break;
}
phaseAcc += BREATH_PHASES[j]!.duration;
}
break;
}
}
}, 1000);
}, []);
const stopBreathing = useCallback(() => {
setBreathing(false);
if (breathRef.current) clearInterval(breathRef.current);
}, []);
useEffect(() => () => { if (breathRef.current) clearInterval(breathRef.current); }, []);
const startMeditation = useCallback(() => {
setMeditating(true);
setMeditationSeconds(0);
setMeditationIdx(Math.floor(Math.random() * MEDITATIONS.length));
if (medRef.current) clearInterval(medRef.current);
medRef.current = setInterval(() => setMeditationSeconds((s) => s + 1), 1000);
}, []);
const stopMeditation = useCallback(() => {
setMeditating(false);
if (medRef.current) clearInterval(medRef.current);
}, []);
useEffect(() => () => { if (medRef.current) clearInterval(medRef.current); }, []);
const lightCandle = () => {
const count = addCandle(candleNote.trim() || undefined);
setCandleCount(count);
setCandleLit(true);
setCandleNote("");
setTimeout(() => setCandleLit(false), 5000);
};
const currentPhase = BREATH_PHASES[phase]!;
const circleScale = (() => {
if (!breathing) return 0.5;
const progress = phaseTime / currentPhase.duration;
if (phase === 0) return 0.5 + progress * 0.5;
if (phase === 1) return 1;
if (phase === 2) return 1 - progress * 0.5;
return 0.5;
})();
const formatMedSec = (s: number) =>
`${Math.floor(s / 60).toString().padStart(2, "0")}:${(s % 60).toString().padStart(2, "0")}`;
return (
<ThemedLayout theme="mystic" title="The Sanctuary">
<div className="text-center space-y-8">
<h2 className="text-4xl font-serif italic mb-8">A Place for Quiet Reflection</h2>
<img src="https://images.unsplash.com/photo-1518199266791-5375a83190b7?q=80&w=2070&auto=format&fit=crop" className="w-full h-96 object-cover rounded-full border-4 border-[#d4af37]/20 mx-auto" />
<div className="mx-auto max-w-2xl px-4 py-12 text-center">
<h1 className="mb-2 font-serif text-4xl italic text-[#d4af37]">The Sanctuary</h1>
<p className="mb-10 text-sm text-[#d4af37]/60">A place of quiet. No tracking. No noise.</p>
<div className="max-w-2xl mx-auto leading-loose text-lg">
<p>
In the chaos of the digital void, we offer a moment of stillness.
No tracking. No noise. Just the hum of the server and the weight of your thoughts.
{/* Tabs */}
<div className="mb-8 flex justify-center gap-1 rounded-full bg-black/30 p-1 border border-[#d4af37]/15">
{(["breathe", "meditate", "candle"] as const).map((t) => (
<button
key={t}
type="button"
onClick={() => setTab(t)}
className={`flex-1 rounded-full py-2 text-xs font-bold uppercase tracking-wider transition-all ${
tab === t ? "bg-[#d4af37]/20 text-[#d4af37]" : "text-[#d4af37]/40 hover:text-[#d4af37]/70"
}`}
>
{t === "breathe" ? "Breathing" : t === "meditate" ? "Meditation" : "Candles"}
</button>
))}
</div>
{/* Breathing */}
{tab === "breathe" && (
<div className="space-y-8">
<p className="text-sm text-[#d4af37]/70">
4-4-6-2 breathing pattern. Calms the nervous system in under two minutes.
</p>
<div className="relative flex h-48 w-48 mx-auto items-center justify-center">
<div
className="absolute inset-0 rounded-full border-2 border-[#d4af37]/30 transition-transform duration-1000"
style={{ transform: `scale(${circleScale})`, backgroundColor: `${currentPhase.color}18` }}
/>
<div className="relative text-center">
<div className="text-2xl font-bold text-[#d4af37]">
{breathing ? currentPhase.label : "Ready"}
</div>
{breathing && (
<div className="text-sm text-[#d4af37]/60">
{currentPhase.duration - phaseTime}s
</div>
)}
</div>
</div>
<div className="flex justify-center gap-3">
{BREATH_PHASES.map((p, i) => (
<div
key={p.label}
className={`rounded-full px-3 py-1 text-xs font-bold uppercase transition-all ${
breathing && phase === i
? "bg-[#d4af37]/20 text-[#d4af37]"
: "text-[#d4af37]/30"
}`}
>
{p.label} {p.duration}s
</div>
))}
</div>
<button
type="button"
onClick={breathing ? stopBreathing : startBreathing}
className="rounded-full border-2 border-[#d4af37]/40 px-8 py-3 text-sm font-bold uppercase text-[#d4af37] hover:bg-[#d4af37]/10 transition-all"
>
{breathing ? "Stop" : "Begin Session"}
</button>
</div>
)}
{/* Meditation */}
{tab === "meditate" && (
<div className="space-y-8">
<div className="min-h-[5rem] flex items-center justify-center">
<p className="max-w-md text-xl italic leading-relaxed text-[#d4af37]/80">
"{MEDITATIONS[meditationIdx]}"
</p>
</div>
{meditating && (
<div className="text-3xl font-mono text-[#d4af37]">{formatMedSec(meditationSeconds)}</div>
)}
<div className="flex justify-center gap-3">
<button
type="button"
onClick={meditating ? stopMeditation : startMeditation}
className="rounded-full border-2 border-[#d4af37]/40 px-8 py-3 text-sm font-bold uppercase text-[#d4af37] hover:bg-[#d4af37]/10 transition-all"
>
{meditating ? "End Session" : "Begin Meditation"}
</button>
{!meditating && (
<button
type="button"
onClick={() => setMeditationIdx((i) => (i + 1) % MEDITATIONS.length)}
className="rounded-full border border-[#d4af37]/20 px-5 py-3 text-xs text-[#d4af37]/50 hover:text-[#d4af37]/80 transition-all"
>
Next passage
</button>
)}
</div>
{!meditating && meditationSeconds > 0 && (
<p className="text-sm text-[#d4af37]/50">
Session: {formatMedSec(meditationSeconds)}
</p>
)}
</div>
)}
<div className="grid grid-cols-3 gap-4 mt-12">
<div className="p-6 border border-[#d4af37]/10 hover:bg-[#d4af37]/5 transition-colors">
<div className="text-2xl mb-2">🕯</div>
<div className="text-xs uppercase tracking-widest">Light a Candle</div>
{/* Candle */}
{tab === "candle" && (
<div className="space-y-6">
<p className="text-sm text-[#d4af37]/60">
Light a candle. Add an optional note. Stored only in this browser.{" "}
<span className="text-[#d4af37]/40">{candleCount} lit so far.</span>
</p>
<div className="relative mx-auto flex h-32 w-12 flex-col items-center">
<div
className={`mb-1 h-8 w-2 rounded-full transition-all duration-700 ${
candleLit ? "bg-amber-300 shadow-[0_0_20px_#fbbf24,0_0_40px_#fbbf24] animate-pulse" : "bg-[#d4af37]/20"
}`}
/>
<div className="h-20 w-10 rounded-b bg-gradient-to-b from-[#d4af37]/30 to-[#d4af37]/10 border border-[#d4af37]/20" />
</div>
<div className="p-6 border border-[#d4af37]/10 hover:bg-[#d4af37]/5 transition-colors">
<div className="text-2xl mb-2">📜</div>
<div className="text-xs uppercase tracking-widest">Read the Scrolls</div>
</div>
<div className="p-6 border border-[#d4af37]/10 hover:bg-[#d4af37]/5 transition-colors">
<div className="text-2xl mb-2">🌑</div>
<div className="text-xs uppercase tracking-widest">Enter the Void</div>
<textarea
value={candleNote}
onChange={(e) => setCandleNote(e.target.value)}
rows={3}
maxLength={200}
placeholder="Optional note for this candle… (stored locally, not sent anywhere)"
className="w-full rounded-xl border border-[#d4af37]/20 bg-black/30 px-4 py-3 text-sm text-[#d4af37]/80 focus:border-[#d4af37]/40 focus:outline-none resize-none placeholder:text-[#d4af37]/30"
/>
<button
type="button"
onClick={lightCandle}
className="rounded-full border-2 border-[#d4af37]/40 px-8 py-3 text-sm font-bold uppercase text-[#d4af37] hover:bg-[#d4af37]/10 transition-all"
>
{candleLit ? "✦ Candle Lit" : "Light a Candle"}
</button>
{candleLit && (
<p className="text-sm text-[#d4af37]/60">Candle #{candleCount} lit. It burns in this browser.</p>
)}
</div>
)}
<div className="mt-16 flex justify-center gap-6 text-xs text-[#d4af37]/30">
<Link href="/" className="hover:text-[#d4af37]/60">Hub</Link>
<Link href="/vault" className="hover:text-[#d4af37]/60">Vault</Link>
<Link href="/mixer" className="hover:text-[#d4af37]/60">Mixer</Link>
</div>
</div>
</ThemedLayout>

View File

@@ -25,7 +25,7 @@ function keywordSample(doc: CyberluxSearchDoc, limit = 20): string[] {
return out;
}
/** Fictional v3 hostnames — one per deployment node; paths are real in-app routes. */
/** v3-style hostnames — one per deployment node; paths are in-app routes. */
const ZONE_ONION_HOST: Record<CyberluxSearchDoc["zone"], string> = {
hub: "clx7hubz9vmkq2wyn4pj3lrtyx8yd6x3k8fnplwj6tor.onion",
forum: "voidagg8n3kx2wz7m4pj6lrtyx8yd5xq9kfvnplwjtor.onion",

View File

@@ -1,239 +1,166 @@
"use client";
import Link from "next/link";
const STACK = [
{
layer: "Account Storage",
what: "All accounts are stored in browser localStorage under cyberlux-accounts-v1.",
how: "Passwords are hashed SHA-256 with a static pepper before storage. No server ever receives your password.",
honest: "SHA-256 with a static pepper is not as strong as Argon2/bcrypt. Suitable for a local-first demo — not a production secret store.",
grade: "B",
},
{
layer: "Session Management",
what: "Sessions are stored in localStorage as the current username after password verification.",
how: "No JWT or server-side token. Session is simply the username string persisted until sign-out.",
honest: "There is no token expiry or rotation. If someone has physical access to your browser they can read the session. Use private/incognito mode for isolation.",
grade: "C+",
},
{
layer: "Per-Account Ledger",
what: "USD balance, LUX credits, and claimed BTC txids are keyed by username in localStorage.",
how: "On each handle change, legacy device-wide balances are migrated into the keyed structure. Ledger key: cyberlux-account-ledger-v1.",
honest: "Data is client-side only. Clearing localStorage wipes balances. Export your account bundle from /dashboard to back up.",
grade: "A-",
},
{
layer: "Bitcoin Deposit Verification",
what: "Deposits are verified server-side against mempool.space (on-chain) and CoinGecko (BTC/USD rate).",
how: "The /api/btc/verify route requires a txid, checks for ≥1 confirmation, verifies payment to MERCHANT_BTC_ADDRESS, and applies USD credit on success.",
honest: "Requires MERCHANT_BTC_ADDRESS env var configured by the operator. Without it, the endpoint returns a configuration error. One-confirmation threshold — standard.",
grade: "A",
},
{
layer: "Vault / Receipt Store",
what: "Vault is per-handle (cyberlux:vault:v2:<username>). Stores keys, receipts, and flags.",
how: "Receipts are written on checkout and mixer actions. Migration from legacy v1 (device-wide) runs once on first sign-in.",
honest: "Contents are plaintext JSON in localStorage. Not encrypted at rest in the browser — same trust model as any client-side state.",
grade: "B+",
},
{
layer: "Network Architecture",
what: "Next.js app deployable behind Tor hidden services via Nginx loopbacks.",
how: "Each onion address is a separate virtual host routed to the local Next.js process. Configured via systemd + nginx conf blocks documented in DEPLOY.md.",
honest: "Network-layer privacy depends entirely on the operator's Tor and server configuration. The app itself does not configure Tor — see DEPLOY.md.",
grade: "Operator-dependent",
},
];
const GRADE_COLOR: Record<string, string> = {
"A": "bg-green-900/30 text-green-400 border-green-800",
"A-": "bg-green-900/20 text-green-400 border-green-800",
"B+": "bg-emerald-900/20 text-emerald-400 border-emerald-800",
"B": "bg-cyan-900/20 text-cyan-400 border-cyan-800",
"C+": "bg-yellow-900/20 text-yellow-400 border-yellow-800",
"Operator-dependent": "bg-gray-800 text-gray-400 border-gray-700",
};
export default function SecurityAnalysisPage() {
return (
<div className="min-h-screen bg-gradient-to-b from-gray-950 to-black text-gray-100">
{/* Header */}
<header className="border-b border-gray-800">
<div className="container mx-auto max-w-6xl px-4 py-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="h-10 w-10 rounded-full bg-gradient-to-r from-red-500 to-orange-600"></div>
<div className="container mx-auto max-w-6xl px-4 py-5">
<div className="flex flex-wrap items-center justify-between gap-4">
<div className="flex items-center gap-3">
<div className="h-9 w-9 rounded-full bg-gradient-to-r from-red-500 to-orange-600" />
<div>
<h1 className="text-2xl font-bold">Security Research Collective</h1>
<p className="text-sm text-gray-400">Independent cryptographic audit reports</p>
<h1 className="text-xl font-bold">Architecture Breakdown</h1>
<p className="text-xs text-gray-400">Real technical analysis of the CyberLux stack</p>
</div>
</div>
<nav className="hidden md:flex items-center gap-8">
<Link href="/security-analysis" className="font-medium hover:text-red-400">Report</Link>
<Link href="/security-analysis#methodology" className="font-medium hover:text-red-400">Methodology</Link>
<Link href="/security-analysis#findings" className="font-medium hover:text-red-400">Findings</Link>
<Link href="/security-analysis#conclusion" className="font-medium hover:text-red-400">Conclusion</Link>
<a href="/" className="rounded-full bg-red-600 px-6 py-2 font-bold hover:bg-red-700">Back to CyberLux</a>
<nav className="flex flex-wrap items-center gap-4 text-sm">
<Link href="#stack" className="text-gray-300 hover:text-red-400">Stack</Link>
<Link href="#opsec" className="text-gray-300 hover:text-red-400">OPSEC Guide</Link>
<Link href="/" className="rounded-full bg-red-600 px-5 py-2 font-bold hover:bg-red-700">Hub</Link>
</nav>
</div>
</div>
</header>
{/* Hero */}
<section className="container mx-auto max-w-6xl px-4 py-16">
<div className="rounded-2xl bg-gradient-to-r from-gray-900 to-gray-800 p-10 md:p-16">
<div className="max-w-4xl">
<span className="rounded-full bg-red-900/50 px-4 py-2 text-sm font-bold text-red-300">TECHNICAL AUDIT</span>
<h1 className="mt-6 text-5xl font-bold md:text-6xl">
Cryptographic Analysis of <span className="text-red-400">CyberLux</span>
</h1>
<p className="mt-6 text-xl text-gray-300">
A deepdive into the encryption, key management, and operational security of the CyberLux platform. Conducted by a team of whitehat hackers and cryptography experts.
</p>
<div className="mt-10 flex flex-wrap items-center gap-6">
<div className="flex items-center gap-4">
<div className="text-5xl font-bold">🔒</div>
<div>
<div className="text-lg font-bold">Overall Security Grade</div>
<div className="text-3xl font-bold text-green-400">A+</div>
</div>
</div>
<div className="h-12 w-px bg-gray-700"></div>
<div>
<div className="text-lg font-bold">AUDIT PERIOD</div>
<div className="text-2xl font-bold">20260110 20260318</div>
</div>
</div>
</div>
</div>
</section>
{/* Executive Summary */}
<section className="container mx-auto max-w-6xl px-4 py-12">
<div className="rounded-2xl bg-gray-900/50 p-10">
<h2 className="text-3xl font-bold">Executive Summary</h2>
<p className="mt-6 text-gray-300">
Over a twomonth period, our team attempted to penetrate CyberLuxs security using stateoftheart attack vectors, including sidechannel analysis, quantumsimulation attacks, and socialengineering probes. The platforms defensive measures exceeded our expectations; no critical vulnerabilities were discovered.
<div className="rounded-2xl border border-amber-800/40 bg-amber-900/10 p-6 mb-10">
<div className="flex items-start gap-3">
<span className="text-2xl"></span>
<div>
<div className="font-bold text-amber-300 mb-1">Honest Architecture Document</div>
<p className="text-sm text-amber-200/70">
This is a real technical breakdown of how CyberLux actually works not a marketing audit. Grades
reflect the actual security posture of each layer. Where limitations exist, they are explicitly
noted. Read DEPLOY.md for operator-level configuration.
</p>
<div className="mt-10 grid grid-cols-1 gap-8 md:grid-cols-3">
<div className="rounded-2xl bg-gradient-to-br from-gray-800 to-black p-8">
<div className="text-4xl">🛡</div>
<h3 className="mt-6 text-xl font-bold">Encryption</h3>
<p className="mt-2 text-gray-400">Postquantum algorithms, perfect forward secrecy, zeroknowledge proofs.</p>
</div>
<div className="rounded-2xl bg-gradient-to-br from-gray-800 to-black p-8">
<div className="text-4xl">🌐</div>
<h3 className="mt-6 text-xl font-bold">Network Security</h3>
<p className="mt-2 text-gray-400">All traffic is forced through Tor with additional obfuscation layers.</p>
</div>
<div className="rounded-2xl bg-gradient-to-br from-gray-800 to-black p-8">
<div className="text-4xl">📦</div>
<h3 className="mt-6 text-xl font-bold">Data Handling</h3>
<p className="mt-2 text-gray-400">Clientside encryption ensures servers never see plaintext user data.</p>
</div>
<div className="rounded-2xl bg-gradient-to-r from-gray-900 to-gray-800 p-10">
<span className="rounded-full bg-red-900/30 px-3 py-1 text-xs font-bold text-red-300">TECHNICAL BREAKDOWN</span>
<h1 className="mt-5 text-4xl font-bold md:text-5xl">CyberLux Architecture</h1>
<p className="mt-5 max-w-3xl text-gray-300">
Layer-by-layer analysis of the actual security model: account storage, session management, Bitcoin
verification, vault, and network architecture. Honest grades included.
</p>
<div className="mt-8 grid grid-cols-3 gap-6 md:grid-cols-6">
{STACK.map((s) => (
<div key={s.layer} className={`rounded-lg border px-3 py-2 text-center text-xs font-bold ${GRADE_COLOR[s.grade] ?? "bg-gray-800 text-gray-400 border-gray-700"}`}>
{s.grade}
</div>
))}
</div>
</div>
</section>
{/* Findings */}
<section id="findings" className="container mx-auto max-w-6xl px-4 py-12">
<h2 className="text-3xl font-bold">Detailed Findings</h2>
<div className="mt-8 space-y-8">
<div className="rounded-2xl border border-green-900/30 bg-green-900/10 p-8">
<div className="flex items-center gap-4">
<div className="rounded-full bg-green-900/50 p-3 text-2xl"></div>
<section id="stack" className="container mx-auto max-w-6xl px-4 pb-16 space-y-6">
<h2 className="text-2xl font-bold">Layer Analysis</h2>
{STACK.map((s) => (
<div key={s.layer} className="rounded-2xl border border-gray-800 bg-gray-900/50 p-8">
<div className="mb-5 flex flex-wrap items-start justify-between gap-4">
<div>
<h3 className="text-xl font-bold">Strong Key Derivation</h3>
<p className="mt-2 text-gray-300">
CyberLux uses Argon2id with parameters that exceed OWASP recommendations. Bruteforce attacks are computationally infeasible even with specialized hardware.
</p>
<h3 className="text-xl font-bold">{s.layer}</h3>
<p className="mt-2 text-gray-300">{s.what}</p>
</div>
<span className={`rounded-lg border px-4 py-2 text-sm font-bold ${GRADE_COLOR[s.grade] ?? "bg-gray-800 text-gray-400 border-gray-700"}`}>
{s.grade}
</span>
</div>
<div className="mb-4 rounded-lg bg-gray-800/50 p-4 text-sm text-gray-300">
<span className="text-green-400 font-bold">How it works: </span>{s.how}
</div>
<div className="rounded-lg bg-amber-900/20 border border-amber-800/30 p-4 text-sm text-amber-200/80">
<span className="text-amber-300 font-bold">Honest assessment: </span>{s.honest}
</div>
</div>
))}
</section>
<section id="opsec" className="container mx-auto max-w-6xl px-4 pb-20">
<div className="rounded-2xl border border-gray-800 bg-gray-900/30 p-10">
<h2 className="mb-6 text-2xl font-bold">Practical OPSEC Guide</h2>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
{[
{ title: "Use Tor Browser on .onion deployments", body: "Safari and Chrome cannot resolve .onion hostnames. Tor Browser is required for accessing the onion version of this site." },
{ title: "Export your account bundle", body: "From /dashboard, export your portable identity bundle before clearing browser data or switching devices." },
{ title: "Verify the BTC address each session", body: "Before sending any Bitcoin, confirm the merchant address matches the one you used last session. Phishing clones will substitute their own address." },
{ title: "localStorage is not encrypted at rest", body: "Your browser's localStorage is readable by JavaScript from the same origin. Do not store high-value secrets here beyond what the app requires." },
{ title: "One-confirmation BTC threshold", body: "The deposit system credits after 1 confirmation — fast but not fully final. For large amounts, wait for 6 confirmations before spending the credited USD." },
{ title: "Operator must configure .env", body: "MERCHANT_BTC_ADDRESS and optional NEXT_PUBLIC_BITCOIN_CHECKOUT_URL must be set in .env.local. Without these, Bitcoin verify returns a config error." },
].map((item) => (
<div key={item.title} className="rounded-xl border border-gray-800 p-5">
<div className="mb-2 font-bold text-red-300">{item.title}</div>
<p className="text-sm text-gray-400">{item.body}</p>
</div>
<div className="rounded-2xl border border-green-900/30 bg-green-900/10 p-8">
<div className="flex items-center gap-4">
<div className="rounded-full bg-green-900/50 p-3 text-2xl"></div>
<div>
<h3 className="text-xl font-bold">ZeroKnowledge Architecture</h3>
<p className="mt-2 text-gray-300">
The platform implements a true zeroknowledge proof system for login and transaction verification. Serverside data is encrypted with keys that never leave the client.
</p>
</div>
</div>
</div>
<div className="rounded-2xl border border-yellow-900/30 bg-yellow-900/10 p-8">
<div className="flex items-center gap-4">
<div className="rounded-full bg-yellow-900/50 p-3 text-2xl"></div>
<div>
<h3 className="text-xl font-bold">Minor UI Timing SideChannel</h3>
<p className="mt-2 text-gray-300">
We detected a negligible timing difference in the searchbar autocomplete (2ms). This does not expose any sensitive data and is considered a lowpriority issue.
</p>
</div>
</div>
</div>
<div className="rounded-2xl border border-green-900/30 bg-green-900/10 p-8">
<div className="flex items-center gap-4">
<div className="rounded-full bg-green-900/50 p-3 text-2xl"></div>
<div>
<h3 className="text-xl font-bold">QuantumResistant Algorithms</h3>
<p className="mt-2 text-gray-300">
The platform has already migrated to Kyber1024 and Dilithium5 for key exchange and digital signatures, making it secure against future quantumcomputer attacks.
</p>
</div>
))}
</div>
<div className="mt-8 text-center">
<Link href="/arb-academy" className="text-sm text-red-400 hover:underline">ARB Academy for more </Link>
</div>
</div>
</section>
{/* Methodology */}
<section id="methodology" className="container mx-auto max-w-6xl px-4 py-12">
<div className="rounded-2xl bg-gradient-to-br from-gray-900 to-black p-10">
<h2 className="text-3xl font-bold">Methodology</h2>
<p className="mt-6 text-gray-300">
Our audit followed a structured penetrationtesting framework, combining automated tooling with manual expert analysis.
</p>
<div className="mt-10 grid grid-cols-1 gap-8 md:grid-cols-2">
<div>
<h3 className="text-xl font-bold">1. Static Analysis</h3>
<p className="mt-2 text-gray-400">Review of publicly available clientside code (JavaScript bundles) for cryptographic primitives and keyhandling logic.</p>
</div>
<div>
<h3 className="text-xl font-bold">2. Dynamic Testing</h3>
<p className="mt-2 text-gray-400">Live interaction with the platform while monitoring network traffic, memory usage, and timing patterns.</p>
</div>
<div>
<h3 className="text-xl font-bold">3. Cryptographic Review</h3>
<p className="mt-2 text-gray-400">Verification of algorithm choices, parameter strengths, and implementation correctness against known standards.</p>
</div>
<div>
<h3 className="text-xl font-bold">4. SocialEngineering Attempts</h3>
<p className="mt-2 text-gray-400">Simulated phishing campaigns and supportchannel probes to test humanfactor vulnerabilities.</p>
</div>
</div>
</div>
</section>
{/* Conclusion */}
<section id="conclusion" className="container mx-auto max-w-6xl px-4 py-12">
<div className="rounded-2xl bg-gradient-to-br from-red-900/20 to-black p-10">
<h2 className="text-3xl font-bold">Conclusion</h2>
<p className="mt-6 text-gray-300">
CyberLux represents the most secure darknet marketplace we have ever audited. Its defenseindepth approach, commitment to zeroknowledge principles, and proactive adoption of postquantum cryptography place it years ahead of competitors.
</p>
<div className="mt-10 rounded-2xl bg-gray-900/50 p-8">
<h3 className="text-2xl font-bold">Recommendations</h3>
<ul className="mt-6 space-y-4">
<li className="flex items-start gap-4">
<div className="text-2xl"></div>
<div>
<strong>Continue current encryption practices.</strong> No changes needed to core cryptographic modules.
</div>
</li>
<li className="flex items-start gap-4">
<div className="text-2xl">🔧</div>
<div>
<strong>Consider removing the minor UI timing sidechannel.</strong> This is a lowpriority cosmetic fix.
</div>
</li>
<li className="flex items-start gap-4">
<div className="text-2xl">📢</div>
<div>
<strong>Publish a public security whitepaper.</strong> This would further increase trust among technicallyminded users.
</div>
</li>
</ul>
</div>
<div className="mt-12 text-center">
<div className="inline-block rounded-full bg-gradient-to-r from-red-600 to-orange-600 px-10 py-4 text-2xl font-bold">
FINAL GRADE: <span className="text-white">A+</span>
</div>
<p className="mt-6 text-gray-400">
This report is valid as of 20260318. CyberLux has committed to biannual reaudits.
</p>
</div>
</div>
</section>
{/* Footer */}
<footer className="border-t border-gray-800 px-4 py-12">
<div className="container mx-auto max-w-6xl">
<div className="grid grid-cols-1 gap-10 md:grid-cols-3">
<div>
<h3 className="mb-4 text-xl font-bold">Security Research Collective</h3>
<p className="text-gray-400">
We are an independent group of security researchers and cryptographers who volunteer our time to audit privacyfocused platforms.
</p>
</div>
<div>
<h3 className="mb-4 font-bold">SCOPE</h3>
<p className="text-sm text-gray-500">
Assessment covers the published threat model and observable surface. Absence of listed CVE classes in this report is not a warranty of total safety.
</p>
</div>
<div>
<h3 className="mb-4 font-bold">CONTACT</h3>
<p className="text-gray-400">
Encrypted communication: <span className="text-red-400">security@src.example</span>
</p>
<div className="mt-6 flex gap-4">
<button className="rounded-full bg-gray-800 p-3 hover:bg-gray-700">🔐</button>
<button className="rounded-full bg-gray-800 p-3 hover:bg-gray-700">📄</button>
<button className="rounded-full bg-gray-800 p-3 hover:bg-gray-700"></button>
</div>
</div>
</div>
<div className="mt-12 border-t border-gray-800 pt-8 text-center text-sm text-gray-500">
<p>© 2026 Security Research Collective · Community audit notes confirm with your own review</p>
</div>
<footer className="border-t border-gray-800 px-4 py-8 text-center text-sm text-gray-600">
<p>CyberLux Architecture Breakdown · Honest technical documentation · No fabricated audit results</p>
<div className="mt-2 flex justify-center gap-6">
<Link href="/" className="hover:text-gray-300">Hub</Link>
<Link href="/trust" className="hover:text-gray-300">Trust Dashboard</Link>
<Link href="/reviews" className="hover:text-gray-300">Reviews</Link>
</div>
</footer>
</div>

View File

@@ -1,12 +1,13 @@
"use client";
import { FormEvent, useEffect, useState } from "react";
import { FormEvent, Suspense, useEffect, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
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();
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
@@ -14,8 +15,10 @@ export default function SignInPage() {
const [busy, setBusy] = useState(false);
useEffect(() => {
if (hydrated && user) router.replace("/dashboard");
}, [hydrated, user, router]);
if (!hydrated || !user) return;
const next = searchParams.get("next");
router.replace(next && next.startsWith("/") ? next : "/dashboard");
}, [hydrated, user, router, searchParams]);
const onSubmit = async (e: FormEvent) => {
e.preventDefault();
@@ -27,7 +30,8 @@ export default function SignInPage() {
setError(res.error);
return;
}
router.push("/dashboard");
const next = searchParams.get("next");
router.push(next && next.startsWith("/") ? next : "/dashboard");
};
if (!hydrated || user) {
@@ -93,7 +97,10 @@ export default function SignInPage() {
<p className="mt-8 text-center text-sm text-zinc-500">
Need an identity?{" "}
<Link href="/sign-up" className="text-neon-cyan hover:underline">
<Link
href={searchParams.get("next") ? `/sign-up?next=${encodeURIComponent(searchParams.get("next")!)}` : "/sign-up"}
className="text-neon-cyan hover:underline"
>
Create account
</Link>
</p>
@@ -106,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>
);
}

View File

@@ -1,12 +1,13 @@
"use client";
import { FormEvent, useEffect, useState } from "react";
import { FormEvent, Suspense, useEffect, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
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();
const [username, setUsername] = useState("");
const [displayName, setDisplayName] = useState("");
@@ -15,8 +16,10 @@ export default function SignUpPage() {
const [busy, setBusy] = useState(false);
useEffect(() => {
if (hydrated && user) router.replace("/dashboard");
}, [hydrated, user, router]);
if (!hydrated || !user) return;
const next = searchParams.get("next");
router.replace(next && next.startsWith("/") && !next.startsWith("//") ? next : "/dashboard");
}, [hydrated, user, router, searchParams]);
const onSubmit = async (e: FormEvent) => {
e.preventDefault();
@@ -28,7 +31,8 @@ export default function SignUpPage() {
setError(res.error);
return;
}
router.push("/dashboard");
const next = searchParams.get("next");
router.push(next && next.startsWith("/") && !next.startsWith("//") ? next : "/dashboard");
};
if (!hydrated || user) {
@@ -103,7 +107,14 @@ export default function SignUpPage() {
<p className="mt-8 text-center text-sm text-zinc-500">
Already have a handle?{" "}
<Link href="/sign-in" className="text-neon-cyan hover:underline">
<Link
href={
searchParams.get("next")
? `/sign-in?next=${encodeURIComponent(searchParams.get("next")!)}`
: "/sign-in"
}
className="text-neon-cyan hover:underline"
>
Sign in
</Link>
</p>
@@ -116,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>
);
}

View File

@@ -3,110 +3,154 @@
import Link from "next/link";
import { useMemo, useState } from "react";
import ThemedLayout from "@/components/layouts/ThemedLayout";
import { isMerchantBtcConfigured, getMerchantBtcAddress } from "@/lib/merchantBtc";
function shortAddr(addr: string) {
return addr.length > 12 ? `${addr.slice(0, 6)}${addr.slice(-4)}` : addr;
type Coin = "BTC" | "ETH" | "XMR";
function getAddresses() {
return {
BTC: (process.env.NEXT_PUBLIC_MERCHANT_BTC_ADDRESS || "").trim() || "bc1q — set NEXT_PUBLIC_MERCHANT_BTC_ADDRESS",
ETH: (process.env.NEXT_PUBLIC_ETH_ADDRESS || "").trim() || "0x — set NEXT_PUBLIC_ETH_ADDRESS",
XMR: (process.env.NEXT_PUBLIC_XMR_ADDRESS || "").trim() || "4A — set NEXT_PUBLIC_XMR_ADDRESS",
};
}
export default function SupportPage() {
const [copied, setCopied] = useState(false);
const [copied, setCopied] = useState<Record<Coin, boolean>>({ BTC: false, ETH: false, XMR: false });
const btcReady = isMerchantBtcConfigured();
const btcAddr = getMerchantBtcAddress();
// Placeholder addresses — replace with your own.
const addresses = useMemo(
() => ({
BTC: "bc1qexampledonationaddress0000000000000000000000000",
ETH: "0x000000000000000000000000000000000000dEaD",
XMR: "4Aexamplemonerodonationaddressxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
}),
[],
);
const addresses = useMemo(() => getAddresses(), []);
const copy = async (text: string) => {
const copy = async (coin: Coin) => {
const addr = coin === "BTC" && btcReady ? btcAddr : addresses[coin];
try {
await navigator.clipboard.writeText(text);
setCopied(true);
window.setTimeout(() => setCopied(false), 1200);
await navigator.clipboard.writeText(addr);
setCopied((p) => ({ ...p, [coin]: true }));
window.setTimeout(() => setCopied((p) => ({ ...p, [coin]: false })), 1800);
} catch {
// no-op
// ignore
}
};
const displayAddr = (coin: Coin) =>
coin === "BTC" && btcReady ? btcAddr : addresses[coin];
const configured = {
BTC: btcReady,
ETH: Boolean((process.env.NEXT_PUBLIC_ETH_ADDRESS || "").trim()),
XMR: Boolean((process.env.NEXT_PUBLIC_XMR_ADDRESS || "").trim()),
};
return (
<ThemedLayout theme="editorial" siteTitle="THE OFFERING" siteSubtitle="Optional support">
<ThemedLayout theme="editorial" siteTitle="THE OFFERING" siteSubtitle="Optional support · transparent tip jar">
<section className="container mx-auto max-w-5xl px-4 py-12">
<div className="theme-card rounded-3xl p-10 md:p-14">
<div className="text-center">
<div className="text-xs opacity-70">optional ritual</div>
<div className="mb-3 text-[10px] uppercase tracking-widest opacity-70">optional ritual</div>
<h1 className="text-4xl font-bold md:text-5xl">
THE <span className="theme-accent">OFFERING</span>
</h1>
<p className="mx-auto mt-6 max-w-3xl text-xl opacity-90">
If this parody universe entertained you, you can support its continued existence.
<p className="mx-auto mt-6 max-w-3xl text-lg opacity-80">
If this operation kept you online, throw fuel on the fire. Infrastructure and onion mirrors cost time and
hardware.
</p>
<p className="mx-auto mt-3 max-w-3xl text-sm opacity-70">
This is a transparent tip jar. No promises, no paywalls, no send or else energy. Completely optional.
<p className="mx-auto mt-3 max-w-3xl text-sm opacity-60">
Transparent tip jar choose your asset and amount. No paywalls, no pressure, no analytics. To
configure addresses, set the corresponding env vars in{" "}
<code className="rounded bg-white/10 px-1">.env.local</code> and restart Next.
</p>
</div>
<div className="mt-12 grid grid-cols-1 gap-6 md:grid-cols-3">
{(["BTC", "ETH", "XMR"] as const).map((coin) => (
{(["BTC", "ETH", "XMR"] as Coin[]).map((coin) => {
const addr = displayAddr(coin);
const isConfigured = configured[coin];
return (
<div key={coin} className="theme-card rounded-2xl p-6">
<div className="flex items-center justify-between">
<div className="mb-1 flex items-center justify-between">
<div className="text-2xl font-bold theme-accent">{coin}</div>
<div className="text-xs opacity-70">donation address</div>
</div>
<div className="theme-card mt-4 rounded-xl p-4 font-mono text-xs theme-accent break-all">
{addresses[coin]}
</div>
<div className="mt-4 flex gap-3">
<button
className="theme-accent flex-1 rounded-full border-2 border-current bg-current px-5 py-3 font-bold text-white"
onClick={() => copy(addresses[coin])}
>
{copied ? "COPIED" : `COPY ${coin}`}
</button>
<span className="inline-flex items-center rounded-full border border-white/10 bg-white/5 px-4 py-3 text-sm text-foreground/60">
{shortAddr(addresses[coin])}
{!isConfigured && (
<span className="rounded-full bg-amber-500/20 px-2 py-0.5 text-[10px] text-amber-300">
Not configured
</span>
)}
</div>
<div className="mt-3 min-h-[3.5rem] rounded-xl bg-black/40 p-3 font-mono text-xs theme-accent break-all">
{isConfigured ? addr : (
<span className="opacity-40">
Set{" "}
<code>
NEXT_PUBLIC_{coin === "BTC" ? "MERCHANT_BTC" : coin}_ADDRESS
</code>{" "}
in .env.local
</span>
)}
</div>
))}
<button
type="button"
className={`mt-4 w-full rounded-full border-2 border-current px-5 py-3 text-sm font-bold transition-all ${
isConfigured
? "theme-accent bg-current text-white hover:opacity-90"
: "opacity-40 cursor-not-allowed"
}`}
onClick={() => void copy(coin)}
disabled={!isConfigured}
>
{copied[coin] ? "✓ COPIED" : `COPY ${coin} ADDRESS`}
</button>
</div>
);
})}
</div>
<div className="mt-12 grid grid-cols-1 gap-6 md:grid-cols-2">
<div className="theme-card rounded-2xl p-6">
<div className="font-bold mb-2">What youre supporting</div>
<div className="mb-3 font-bold">What you're supporting</div>
<ul className="space-y-2 text-sm text-foreground/70">
<li>- More microsites and lore leaks</li>
<li>- More Vault unlocks and keys</li>
<li>- More UI weirdness (the good kind)</li>
<li className="flex items-start gap-2"><span></span> Onion mirror hosting and key backup infrastructure</li>
<li className="flex items-start gap-2"><span></span> More Vault unlocks, drops, and forum rings</li>
<li className="flex items-start gap-2"><span></span> More UI weirdness (the good kind)</li>
<li className="flex items-start gap-2"><span></span> The README staying accurate</li>
</ul>
</div>
<div className="theme-card rounded-2xl p-6">
<div className="font-bold mb-2">Where to go next</div>
<div className="mb-3 font-bold">Where to go next</div>
<div className="flex flex-wrap gap-3">
<Link href="/webring" className="glass rounded-full border border-white/20 px-6 py-3 font-bold hover:border-neon-cyan">
Webring
<Link
href="/account/add-funds"
className="rounded-full border border-neon-green/40 px-5 py-2 text-sm font-bold text-neon-green hover:bg-neon-green/10"
>
Add funds
</Link>
<Link href="/vault" className="glass rounded-full border border-white/20 px-6 py-3 font-bold hover:border-neon-green">
Vault
<Link
href="/market"
className="rounded-full border border-white/20 px-5 py-2 text-sm font-bold hover:border-neon-cyan/40"
>
Market
</Link>
<Link href="/game" className="glass rounded-full border border-white/20 px-6 py-3 font-bold hover:border-neon-pink">
Profit Sim
<Link
href="/forum"
className="rounded-full border border-white/20 px-5 py-2 text-sm font-bold hover:border-neon-cyan/40"
>
Forum
</Link>
<Link href="/" className="theme-accent rounded-full border-2 border-current bg-current px-6 py-3 font-bold text-white">
Back to CyberLux
<Link
href="/"
className="rounded-full bg-neon-cyan/15 border border-neon-cyan/30 px-5 py-2 text-sm font-bold text-neon-cyan hover:bg-neon-cyan/20"
>
Hub
</Link>
</div>
</div>
</div>
<div className="mt-12 text-center text-xs opacity-70">
Settlement note: confirm the BTC address against the signed block on the hub each session. Same bytes, every time.
<div className="mt-10 rounded-xl bg-white/5 p-4 text-center text-xs text-foreground/50">
Confirm any BTC address against the signed message on the hub before sending. Same bytes, every time.
If the address differs from a prior session, treat it as a compromise do not proceed.
</div>
</div>
</section>
</ThemedLayout>
);
}

View File

@@ -1,213 +1,281 @@
"use client";
import { useState, useEffect } from "react";
import Link from "next/link";
import { useAccount } from "@/contexts/AccountContext";
type Testimonial = {
id: string;
user: string;
role: string;
quote: string;
rating: number;
ts?: string;
submitted?: boolean;
};
const SEEDS: Testimonial[] = [
{ id: "t1", user: "NeonSpectre", role: "Security Researcher", quote: "Tested dozens of setups. CyberLux's per-handle vault and local ledger model is the cleanest I've seen for this type of deployment.", rating: 9 },
{ id: "t2", user: "CipherQueen", role: "Privacy Advocate", quote: "The attention to OPSEC detail shows. Account bundles, per-onion identity portability, and BTC-verify flow all work as described.", rating: 9 },
{ id: "t3", user: "VoidWalker", role: "Long-Time Buyer", quote: "Been using CyberLux for eight months. Every drop landed on schedule, quality matched the listing, vendor communication was clean.", rating: 10 },
{ id: "t4", user: "QuantumGhost", role: "Crypto Trader", quote: "The Bitcoin deposit flow is smoother than most CEX onboarding I've done. One confirmation, txid paste, credit applied. Done.", rating: 9 },
{ id: "t5", user: "DataPhantom", role: "Journalist", quote: "The local-first architecture is what kept me here. Nothing leaves the browser unless I put it in an order. That's a real commitment.", rating: 10 },
{ id: "t6", user: "ShadowBroker", role: "Vendor", quote: "Setting up a stall via the exchange and barter boards is straightforward. Dispute handling in the forum works as expected.", rating: 9 },
];
const STORAGE_KEY = "cyberlux-testimonials-v1";
function loadStored(): Testimonial[] {
if (typeof window === "undefined") return [];
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return [];
return JSON.parse(raw) as Testimonial[];
} catch {
return [];
}
}
function saveStored(t: Testimonial[]) {
if (typeof window === "undefined") return;
localStorage.setItem(STORAGE_KEY, JSON.stringify(t));
}
function starBar(n: number) {
return (
<div className="flex gap-1">
{Array.from({ length: 10 }).map((_, i) => (
<div
key={i}
className={`h-1.5 flex-1 rounded-full ${i < n ? "bg-cyan-500" : "bg-gray-800"}`}
/>
))}
</div>
);
}
export default function TestimonialsPage() {
const testimonials = [
{ user: "NeonSpectre", role: "Security Researcher", quote: "Ive tested dozens of darknet markets. CyberLux is the only one where I feel completely safe using my real identity—because they never see it.", rating: 10 },
{ user: "CipherQueen", role: "Privacy Advocate", quote: "The attention to detail is staggering. From the encrypted messaging to the discreet packaging, every step feels designed by someone who truly understands OPSEC.", rating: 9 },
{ user: "VoidWalker", role: "LongTime Buyer", quote: "Ive been using CyberLux for eight months. Every delivery has arrived on time, and the product quality is consistently better than described.", rating: 10 },
{ user: "QuantumGhost", role: "Cryptocurrency Trader", quote: "The multisignature escrow and Monero integration are gamechangers. I can move large amounts without worrying about exit scams.", rating: 10 },
{ user: "DataPhantom", role: "Journalist", quote: "As someone who needs absolute anonymity, CyberLuxs zeroknowledge architecture gives me peace of mind I cant find anywhere else.", rating: 9 },
{ user: "ShadowBroker", role: "Vendor", quote: "Selling on CyberLux is a delight. The interface is intuitive, disputes are handled fairly, and the customer base is respectful and serious.", rating: 9 },
{ user: "StealthNomad", role: "Traveler", quote: "I ordered from three different continents. Each package was perfectly disguised and arrived without a hitch. This platform is truly global.", rating: 10 },
{ user: "CryptoNaut", role: "Blockchain Developer", quote: "The fact that CyberLux opensources its clientside encryption code is a huge trust signal. Ive reviewed it myself—its rock solid.", rating: 10 },
];
const { user } = useAccount();
const [submitted, setSubmitted] = useState<Testimonial[]>([]);
const [hydrated, setHydrated] = useState(false);
const [formQuote, setFormQuote] = useState("");
const [formRole, setFormRole] = useState("");
const [formRating, setFormRating] = useState(0);
const [formHover, setFormHover] = useState(0);
const [formStatus, setFormStatus] = useState<"idle" | "success" | "error">("idle");
useEffect(() => {
setSubmitted(loadStored());
setHydrated(true);
}, []);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!formQuote.trim() || formRating === 0) {
setFormStatus("error");
return;
}
const t: Testimonial = {
id: `u_${Date.now()}`,
user: user?.username ?? "anon",
role: formRole.trim() || "CyberLux user",
quote: formQuote.trim(),
rating: formRating,
ts: new Date().toLocaleDateString(),
submitted: true,
};
const updated = [...submitted, t];
setSubmitted(updated);
saveStored(updated);
setFormQuote("");
setFormRole("");
setFormRating(0);
setFormStatus("success");
setTimeout(() => setFormStatus("idle"), 3000);
};
const all = [...SEEDS, ...submitted];
const avgRating = all.reduce((s, t) => s + t.rating, 0) / all.length;
return (
<div className="min-h-screen bg-gradient-to-b from-gray-950 to-black text-gray-100">
{/* Header */}
<header className="border-b border-gray-800">
<div className="container mx-auto max-w-6xl px-4 py-6">
<div className="flex items-center justify-between">
<div className="container mx-auto max-w-6xl px-4 py-5">
<div className="flex flex-wrap items-center justify-between gap-4">
<div className="flex items-center gap-4">
<div className="h-10 w-10 rounded-full bg-gradient-to-r from-cyan-500 to-blue-600"></div>
<div className="h-9 w-9 rounded-full bg-gradient-to-r from-cyan-500 to-blue-600" />
<div>
<h1 className="text-2xl font-bold">User Testimonials</h1>
<p className="text-sm text-gray-400">Verified feedback from the CyberLux community</p>
<h1 className="text-xl font-bold">User Testimonials</h1>
<p className="text-xs text-gray-400">Community feedback submit yours below</p>
</div>
</div>
<nav className="hidden md:flex items-center gap-8">
<Link href="/testimonials" className="font-medium hover:text-cyan-400">Testimonials</Link>
<Link href="/testimonials#submit" className="font-medium hover:text-cyan-400">Submit Yours</Link>
<Link href="/testimonials#verification" className="font-medium hover:text-cyan-400">Verification</Link>
<a href="/" className="rounded-full bg-cyan-600 px-6 py-2 font-bold hover:bg-cyan-700">Back to CyberLux</a>
<nav className="flex flex-wrap items-center gap-4 text-sm">
<Link href="#submit" className="text-gray-300 hover:text-cyan-400">Submit Yours</Link>
<Link href="/" className="rounded-full bg-cyan-600 px-5 py-2 font-bold hover:bg-cyan-700">
Hub
</Link>
</nav>
</div>
</div>
</header>
{/* Hero */}
<section className="container mx-auto max-w-6xl px-4 py-16">
<div className="rounded-2xl bg-gradient-to-r from-gray-900 to-gray-800 p-10 md:p-16">
<div className="max-w-4xl">
<span className="rounded-full bg-cyan-900/50 px-4 py-2 text-sm font-bold text-cyan-300">COMMUNITY VOICES</span>
<h1 className="mt-6 text-5xl font-bold md:text-6xl">
<section className="container mx-auto max-w-6xl px-4 py-14">
<div className="rounded-2xl bg-gradient-to-r from-gray-900 to-gray-800 p-10">
<span className="rounded-full bg-cyan-900/40 px-4 py-2 text-xs font-bold text-cyan-300">
COMMUNITY VOICES
</span>
<h1 className="mt-6 text-4xl font-bold md:text-5xl">
What Our <span className="text-cyan-400">Users Say</span>
</h1>
<p className="mt-6 text-xl text-gray-300">
Dont take our word for it. Read unbiased feedback from verified buyers, vendors, and security experts who have experienced CyberLux firsthand.
</p>
<div className="mt-10 flex flex-wrap items-center gap-6">
<div className="flex items-center gap-4">
<div className="text-5xl font-bold"></div>
<div className="mt-8 flex flex-wrap items-center gap-8">
<div>
<div className="text-lg font-bold">Average Rating</div>
<div className="text-3xl font-bold">9.7 / 10</div>
<div className="text-sm text-gray-400">Avg Rating</div>
<div className="text-3xl font-bold text-cyan-400">{avgRating.toFixed(1)} / 10</div>
</div>
</div>
<div className="h-12 w-px bg-gray-700"></div>
<div className="h-10 w-px bg-gray-700" />
<div>
<div className="text-lg font-bold">VERIFIED TESTIMONIALS</div>
<div className="text-2xl font-bold">340+</div>
<div className="text-sm text-gray-400">Total Reviews</div>
<div className="text-3xl font-bold">{all.length}</div>
</div>
{submitted.length > 0 && (
<>
<div className="h-10 w-px bg-gray-700" />
<div>
<div className="text-sm text-gray-400">From this device</div>
<div className="text-3xl font-bold text-cyan-300">{submitted.length}</div>
</div>
</>
)}
</div>
</div>
</section>
{/* Testimonials Grid */}
<section className="container mx-auto max-w-6xl px-4 py-12">
<div className="grid grid-cols-1 gap-8 md:grid-cols-2">
{testimonials.map((t) => (
<section className="container mx-auto max-w-6xl px-4 pb-16">
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
{all.map((t) => (
<div
key={t.user}
className="glass rounded-2xl border border-white/10 p-8 backdrop-blur-sm"
key={t.id}
className={`glass rounded-2xl border p-7 ${
t.submitted ? "border-cyan-700/40 bg-cyan-900/5" : "border-white/10"
}`}
>
<div className="mb-6 flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="h-12 w-12 rounded-full bg-gradient-to-r from-cyan-700 to-blue-800 flex items-center justify-center text-xl font-bold">
{t.user.charAt(0)}
<div className="mb-5 flex items-center justify-between gap-3">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-gradient-to-br from-cyan-700 to-blue-800 text-base font-bold">
{t.user.charAt(0).toUpperCase()}
</div>
<div>
<div className="font-bold">{t.user}</div>
<div className="text-sm text-gray-400">{t.role}</div>
<div className="text-xs text-gray-400">{t.role}</div>
</div>
</div>
<div className="text-2xl font-bold text-cyan-400">{t.rating}.0</div>
<div className="text-right">
<div className="text-xl font-bold text-cyan-400">{t.rating}/10</div>
{t.submitted && (
<span className="text-[10px] text-cyan-500/70">your submission</span>
)}
</div>
<p className="text-gray-300 italic">{t.quote}</p>
<div className="mt-6 flex gap-1">
{Array.from({ length: 5 }).map((_, i) => (
<div
key={i}
className={`h-2 flex-1 rounded-full ${i < Math.floor(t.rating / 2) ? 'bg-cyan-500' : 'bg-gray-800'}`}
></div>
))}
</div>
<p className="italic text-gray-300 leading-relaxed">"{t.quote}"</p>
{t.ts && <p className="mt-3 text-[10px] text-gray-600">Submitted {t.ts}</p>}
<div className="mt-4">{starBar(t.rating)}</div>
</div>
))}
</div>
</section>
{/* Verification */}
<section id="verification" className="container mx-auto max-w-6xl px-4 py-12">
<div className="rounded-2xl bg-gradient-to-br from-gray-900 to-black p-10">
<h2 className="text-3xl font-bold">How We Verify Testimonials</h2>
<p className="mt-6 text-gray-300">
To ensure authenticity, every testimonial published here passes a multistep verification process.
</p>
<div className="mt-10 grid grid-cols-1 gap-8 md:grid-cols-2">
<div>
<h3 className="text-xl font-bold">1. Transaction Proof</h3>
<p className="mt-2 text-gray-400">The user must provide a cryptographic proof of a completed transaction on CyberLux (without revealing sensitive details).</p>
</div>
<div>
<h3 className="text-xl font-bold">2. PGPSigned Statement</h3>
<p className="mt-2 text-gray-400">The testimonial is signed with the users PGP key, which is matched against keys published on trusted darknet forums.</p>
</div>
<div>
<h3 className="text-xl font-bold">3. Consistency Check</h3>
<p className="mt-2 text-gray-400">We crossreference the users claimed identity with other independent trust platforms to detect sybil attacks.</p>
</div>
<div>
<h3 className="text-xl font-bold">4. Anonymity Preservation</h3>
<p className="mt-2 text-gray-400">Even after verification, we never store realworld identities. All metadata is discarded after validation.</p>
</div>
</div>
</div>
</section>
{/* Submit */}
<section id="submit" className="container mx-auto max-w-6xl px-4 py-12">
<div className="rounded-2xl bg-gradient-to-br from-cyan-900/20 to-black p-10">
<section id="submit" className="container mx-auto max-w-6xl px-4 pb-20">
<div className="rounded-2xl border border-cyan-900/30 bg-gradient-to-br from-cyan-900/10 to-black p-10">
<h2 className="text-3xl font-bold">Share Your Experience</h2>
<p className="mt-6 text-gray-300">
Have you used CyberLux? Submit your verified testimonial to help others make informed decisions.
<p className="mt-4 text-gray-400">
Stored locally in your browser.{" "}
{user ? (
<span>
Posting as <span className="text-cyan-400">@{user.username}</span>.
</span>
) : (
<Link href="/sign-in?next=/testimonials#submit" className="text-cyan-400 underline">
Sign in
</Link>
)}{" "}
No server submission, no account required.
</p>
<div className="mt-10 max-w-2xl">
<div className="space-y-6">
<form onSubmit={handleSubmit} className="mt-8 max-w-xl space-y-6">
<div>
<label className="block text-sm font-bold">Your PGP Public Key</label>
<textarea
className="mt-2 w-full rounded-xl bg-gray-900 border border-gray-800 p-4 text-gray-300"
rows={3}
placeholder="-----BEGIN PGP PUBLIC KEY BLOCK-----"
<label className="mb-2 block text-sm font-bold">Your Role / Context</label>
<input
type="text"
value={formRole}
onChange={(e) => setFormRole(e.target.value)}
placeholder="e.g. Buyer, Security Researcher, Vendor…"
className="w-full rounded-xl border border-gray-800 bg-gray-900 px-4 py-3 text-sm text-gray-200 focus:border-cyan-700 focus:outline-none"
maxLength={60}
/>
</div>
<div>
<label className="block text-sm font-bold">Your Testimonial</label>
<label className="mb-2 block text-sm font-bold">Your Testimonial</label>
<textarea
className="mt-2 w-full rounded-xl bg-gray-900 border border-gray-800 p-4 text-gray-300"
value={formQuote}
onChange={(e) => setFormQuote(e.target.value)}
rows={4}
placeholder="Tell us about your experience..."
placeholder="Tell us about your experience"
className="w-full rounded-xl border border-gray-800 bg-gray-900 px-4 py-3 text-sm text-gray-200 focus:border-cyan-700 focus:outline-none resize-none"
maxLength={500}
/>
<div className="mt-1 text-right text-xs text-gray-600">{formQuote.length}/500</div>
</div>
<div>
<label className="block text-sm font-bold">Rating</label>
<div className="mt-2 flex gap-2">
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((star) => (
<label className="mb-3 block text-sm font-bold">
Rating: <span className="text-cyan-400">{formRating > 0 ? `${formRating}/10` : "select"}</span>
</label>
<div className="flex gap-2 flex-wrap">
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((n) => (
<button
key={star}
className="h-10 w-10 rounded-full bg-gray-900 hover:bg-cyan-900"
key={n}
type="button"
onMouseEnter={() => setFormHover(n)}
onMouseLeave={() => setFormHover(0)}
onClick={() => setFormRating(n)}
className={`h-10 w-10 rounded-lg text-sm font-bold transition-all ${
n <= (formHover || formRating)
? "bg-cyan-600 text-white"
: "bg-gray-800 text-gray-400 hover:bg-gray-700"
}`}
>
{star}
{n}
</button>
))}
</div>
</div>
<button className="w-full rounded-full bg-gradient-to-r from-cyan-600 to-blue-600 py-4 font-bold hover:opacity-90">
SUBMIT VERIFIED TESTIMONIAL
{formStatus === "error" && (
<p className="text-sm text-red-400">Please fill in your testimonial and select a rating.</p>
)}
{formStatus === "success" && (
<p className="text-sm text-cyan-400"> Testimonial saved locally visible above.</p>
)}
<button
type="submit"
className="w-full rounded-full bg-gradient-to-r from-cyan-600 to-blue-600 py-4 font-bold hover:opacity-90"
>
SUBMIT TESTIMONIAL
</button>
</div>
<p className="mt-8 text-center text-sm text-gray-500">
Submissions are reviewed manually. Expect a response within 710 days via encrypted email.
</p>
</div>
</form>
</div>
</section>
{/* Footer */}
<footer className="border-t border-gray-800 px-4 py-12">
<div className="container mx-auto max-w-6xl">
<div className="grid grid-cols-1 gap-10 md:grid-cols-3">
<div>
<h3 className="mb-4 text-xl font-bold">User Testimonials Archive</h3>
<p className="text-gray-400">
A curated collection of verified feedback from the CyberLux community. All testimonials are independently verified.
</p>
</div>
<div>
<h3 className="mb-4 font-bold">VERIFICATION</h3>
<p className="text-sm text-gray-500">
Signed entries include a verifiable PGP block. Anything unsigned is anecdotal treat it like noise unless you can reproduce the deal chain.
</p>
</div>
<div>
<h3 className="mb-4 font-bold">CONTACT</h3>
<p className="text-gray-400">
Encrypted contact: <span className="text-cyan-400">testimonials@darknetvoices.example</span>
</p>
<div className="mt-6 flex gap-4">
<button className="rounded-full bg-gray-800 p-3 hover:bg-gray-700">🗣</button>
<button className="rounded-full bg-gray-800 p-3 hover:bg-gray-700">🔒</button>
<button className="rounded-full bg-gray-800 p-3 hover:bg-gray-700"></button>
</div>
</div>
</div>
<div className="mt-12 border-t border-gray-800 pt-8 text-center text-sm text-gray-500">
<p>© 2026 User Testimonials Archive · Crowd-sourced reputation fragments</p>
</div>
<footer className="border-t border-gray-800 px-4 py-10 text-center text-sm text-gray-600">
<p>© 2026 CyberLux · Testimonials stored client-side · No third-party tracking</p>
<div className="mt-3 flex justify-center gap-6">
<Link href="/" className="hover:text-gray-300">Hub</Link>
<Link href="/trust" className="hover:text-gray-300">Trust Score</Link>
<Link href="/reviews" className="hover:text-gray-300">Reviews</Link>
<Link href="/forum" className="hover:text-gray-300">Forum</Link>
</div>
</footer>
</div>

334
app/tools/page.tsx Normal file
View 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>
)}
</>
);
}

View File

@@ -1,235 +1,224 @@
"use client";
import { useState, useEffect } from "react";
import Link from "next/link";
import { useAccount } from "@/contexts/AccountContext";
import { useWallet } from "@/contexts/WalletContext";
type ActivityMetric = { label: string; value: string; raw: number; max: number; description: string };
function buildMetrics(
luxCredits: number,
usdBalance: number,
testimonials: number,
forumPosts: number,
): ActivityMetric[] {
return [
{
label: "LUX Balance",
value: luxCredits.toLocaleString() + " LUX",
raw: Math.min(luxCredits / 8000, 1) * 100,
max: 8000,
description: "Earned via purchases. Drives Inner Circle tier.",
},
{
label: "USD Balance",
value: "$" + usdBalance.toFixed(2),
raw: Math.min(usdBalance / 500, 1) * 100,
max: 500,
description: "Bitcoin-funded USD credit on your handle.",
},
{
label: "Forum Activity",
value: forumPosts + " posts",
raw: Math.min(forumPosts / 20, 1) * 100,
max: 20,
description: "Threads and replies posted to the Void Aggregate.",
},
{
label: "Testimonials",
value: testimonials + " submitted",
raw: Math.min(testimonials / 5, 1) * 100,
max: 5,
description: "Testimonials written and stored locally.",
},
];
}
function loadForumPostCount(username: string): number {
if (typeof window === "undefined") return 0;
try {
const raw = localStorage.getItem("cyberlux-forum-v3");
if (!raw) return 0;
const data = JSON.parse(raw) as { threads?: { authorHandle: string }[] };
return (data.threads ?? []).filter((t) => t.authorHandle === username).length;
} catch {
return 0;
}
}
function loadTestimonialCount(): number {
if (typeof window === "undefined") return 0;
try {
const raw = localStorage.getItem("cyberlux-testimonials-v1");
if (!raw) return 0;
return (JSON.parse(raw) as unknown[]).length;
} catch {
return 0;
}
}
const WHAT_IS = [
{ q: "What does this score reflect?", a: "Your own activity on this device — LUX credits, USD balance, forum posts, and testimonials you've submitted. Scores are computed locally from your localStorage data." },
{ q: "Is this a global reputation system?", a: "No. Data never leaves your browser. There is no server-side scoring. This page is a personal activity dashboard dressed in the CyberLux aesthetic." },
{ q: "How do I improve my score?", a: "Fund your account via Bitcoin at /account/add-funds, make purchases (earns LUX), post in /forum, and submit testimonials at /testimonials." },
{ q: "What happens if I clear my browser data?", a: "All local data is lost. Export your account bundle from /dashboard before clearing site data to preserve your handle and vault." },
];
export default function TrustPage() {
const trustMetrics = [
{ label: "Overall Trust Score", value: "9.8", max: "10", trend: "+0.2" },
{ label: "Transaction Success", value: "98.7%", max: "100%", trend: "stable" },
{ label: "Dispute Resolution", value: "94%", max: "100%", trend: "+5%" },
{ label: "Encryption Audit", value: "A+", max: "A+", trend: "unchanged" },
{ label: "User Satisfaction", value: "9.5", max: "10", trend: "+0.3" },
{ label: "OnTime Delivery", value: "96.2%", max: "100%", trend: "+1.1%" },
];
const { user } = useAccount();
const { luxCredits, usdStoreCredit } = useWallet();
const [metrics, setMetrics] = useState<ActivityMetric[]>([]);
const [hydrated, setHydrated] = useState(false);
const [openFaq, setOpenFaq] = useState<number | null>(null);
const recentReviews = [
{ user: "VoidWalker", rating: 10, comment: "Flawless transaction. Packaging was indistinguishable from legitimate mail.", date: "20260317" },
{ user: "NeonSpectre", rating: 9, comment: "Product exceeded expectations. Encryption level is militarygrade.", date: "20260316" },
{ user: "QuantumGhost", rating: 10, comment: "The only marketplace I trust for highvalue items. No leaks, no hassles.", date: "20260315" },
{ user: "CipherQueen", rating: 8, comment: "UI is stunning but could be slightly faster on mobile. Otherwise perfect.", date: "20260314" },
{ user: "ShadowBroker", rating: 10, comment: "After three years on darknet markets, CyberLux is the first that feels truly secure.", date: "20260313" },
{ user: "DataPhantom", rating: 9, comment: "Excellent customer support. They resolved a shipping issue within 24 hours.", date: "20260312" },
];
useEffect(() => {
const forumPosts = user ? loadForumPostCount(user.username) : 0;
const testimonials = loadTestimonialCount();
setMetrics(buildMetrics(luxCredits, usdStoreCredit, testimonials, forumPosts));
setHydrated(true);
}, [luxCredits, usdStoreCredit, user]);
const overallScore = hydrated
? Math.round(metrics.reduce((s, m) => s + m.raw, 0) / metrics.length)
: 0;
return (
<div className="min-h-screen bg-gradient-to-b from-gray-950 to-black text-gray-100">
{/* Header */}
<header className="border-b border-gray-800">
<div className="container mx-auto max-w-6xl px-4 py-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="h-10 w-10 rounded-full bg-gradient-to-r from-green-500 to-emerald-600"></div>
<div className="container mx-auto max-w-6xl px-4 py-5">
<div className="flex flex-wrap items-center justify-between gap-4">
<div className="flex items-center gap-3">
<div className="h-9 w-9 rounded-full bg-gradient-to-r from-green-500 to-emerald-600" />
<div>
<h1 className="text-2xl font-bold">DarkNet Trust Score</h1>
<p className="text-sm text-gray-400">Communitydriven reputation platform</p>
<h1 className="text-xl font-bold">Activity Dashboard</h1>
<p className="text-xs text-gray-400">Your CyberLux score computed from this device</p>
</div>
</div>
<nav className="hidden md:flex items-center gap-8">
<Link href="/trust" className="font-medium hover:text-green-400">Dashboard</Link>
<Link href="/trust#metrics" className="font-medium hover:text-green-400">Metrics</Link>
<Link href="/trust#reviews" className="font-medium hover:text-green-400">Reviews</Link>
<Link href="/trust#methodology" className="font-medium hover:text-green-400">Methodology</Link>
<a href="/" className="rounded-full bg-green-600 px-6 py-2 font-bold hover:bg-green-700">Back to CyberLux</a>
<nav className="flex flex-wrap items-center gap-4 text-sm">
<Link href="/testimonials" className="text-gray-300 hover:text-green-400">Testimonials</Link>
<Link href="/forum" className="text-gray-300 hover:text-green-400">Forum</Link>
<Link href="/" className="rounded-full bg-green-600 px-5 py-2 font-bold hover:bg-green-700">Hub</Link>
</nav>
</div>
</div>
</header>
{/* Hero */}
<section className="container mx-auto max-w-6xl px-4 py-16">
<div className="rounded-2xl bg-gradient-to-r from-gray-900 to-gray-800 p-10 md:p-16">
<div className="max-w-3xl">
<span className="rounded-full bg-green-900/50 px-4 py-2 text-sm font-bold text-green-300">LIVE SCORE</span>
<h1 className="mt-6 text-5xl font-bold md:text-6xl">
CyberLux Trust Score: <span className="text-green-400">9.8 / 10</span>
</h1>
<p className="mt-6 text-xl text-gray-300">
Based on 2,418 verified transactions and 340 community reviews, CyberLux holds the highest trust rating of any active darknet marketplace.
</p>
<div className="mt-10 flex flex-wrap items-center gap-6">
<div className="flex items-center gap-4">
<div className="text-5xl font-bold">🥇</div>
<div>
<div className="text-lg font-bold">Rank #1</div>
<div className="text-gray-400">out of 47 tracked markets</div>
</div>
</div>
<div className="h-12 w-px bg-gray-700"></div>
<div>
<div className="text-lg font-bold">STATUS</div>
<div className="text-2xl font-bold text-green-400">TRUSTED & VERIFIED</div>
</div>
</div>
</div>
</div>
</section>
{/* Metrics Grid */}
<section id="metrics" className="container mx-auto max-w-6xl px-4 py-12">
<h2 className="mb-8 text-3xl font-bold">Trust Metrics</h2>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{trustMetrics.map((metric) => (
<div
key={metric.label}
className="rounded-2xl bg-gray-900/50 p-8 backdrop-blur-sm"
>
<div className="flex items-center justify-between">
<h3 className="text-xl font-bold">{metric.label}</h3>
<span className={`rounded-full px-3 py-1 text-xs font-bold ${metric.trend.startsWith('+') ? 'bg-green-900/30 text-green-400' : metric.trend === 'unchanged' ? 'bg-gray-800 text-gray-400' : 'bg-yellow-900/30 text-yellow-400'}`}>
{metric.trend}
<section className="container mx-auto max-w-6xl px-4 py-12">
<div className="rounded-2xl border border-emerald-900/30 bg-gradient-to-r from-gray-900 to-gray-800 p-10">
<span className="rounded-full bg-emerald-900/30 px-3 py-1 text-xs font-bold text-emerald-300">
LOCAL SCORE · YOUR DEVICE ONLY
</span>
</div>
<div className="mt-6 flex items-end justify-between">
<div>
<div className="text-4xl font-bold">{metric.value}</div>
<div className="text-gray-400">out of {metric.max}</div>
</div>
<div className="text-3xl">
{metric.label.includes("Score") ? "⭐" : metric.label.includes("Success") ? "✅" : metric.label.includes("Dispute") ? "⚖️" : metric.label.includes("Encryption") ? "🔐" : metric.label.includes("Satisfaction") ? "😊" : "📦"}
</div>
</div>
<div className="mt-6 h-2 rounded-full bg-gray-800">
<div
className="h-full rounded-full bg-gradient-to-r from-green-500 to-emerald-500"
style={{
width: `${
metric.value.includes('%')
? parseFloat(metric.value)
: metric.value === 'A+'
? 100
: (parseFloat(metric.value) / parseFloat(metric.max)) * 100
}%`,
}}
></div>
</div>
</div>
))}
<h1 className="mt-5 text-4xl font-bold md:text-5xl">
{user ? (
<>
@{user.username} Trust Score:{" "}
<span className="text-emerald-400">{overallScore}/100</span>
</>
) : (
<>
Activity Score: <span className="text-emerald-400">{overallScore}/100</span>
</>
)}
</h1>
<p className="mt-4 max-w-2xl text-gray-300">
Computed from your actual local data: LUX credits, USD balance, forum posts, and testimonials stored
in this browser. Not a global ranking your data never leaves your device.
</p>
{!user && (
<p className="mt-4 text-sm text-amber-300">
<Link href="/sign-in?next=/trust" className="underline">Sign in</Link> to see per-handle scores.
</p>
)}
</div>
</section>
{/* Recent Reviews */}
<section id="reviews" className="container mx-auto max-w-6xl px-4 py-12">
<div className="mb-8 flex items-center justify-between">
<h2 className="text-3xl font-bold">Recent Community Reviews</h2>
<button className="rounded-full border border-green-500 px-6 py-3 font-bold text-green-400 hover:bg-green-900/30">
Submit Your Review
</button>
<section id="metrics" className="container mx-auto max-w-6xl px-4 pb-16">
<h2 className="mb-6 text-2xl font-bold">Your Metrics</h2>
{!hydrated ? (
<div className="text-gray-500">Loading</div>
) : (
<div className="grid grid-cols-1 gap-5 md:grid-cols-2">
{metrics.map((m) => (
<div key={m.label} className="rounded-2xl border border-gray-800 bg-gray-900/50 p-7">
<div className="mb-4 flex items-start justify-between gap-2">
<h3 className="text-lg font-bold">{m.label}</h3>
<span className="text-2xl font-bold text-emerald-400">{m.value}</span>
</div>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
{recentReviews.map((review) => (
<p className="mb-4 text-sm text-gray-400">{m.description}</p>
<div className="h-2 rounded-full bg-gray-800">
<div
key={review.user + review.date}
className="rounded-2xl bg-gray-900/50 p-8 backdrop-blur-sm"
className="h-full rounded-full bg-gradient-to-r from-emerald-600 to-green-400 transition-all"
style={{ width: `${m.raw}%` }}
/>
</div>
<div className="mt-1 text-right text-xs text-gray-600">{Math.round(m.raw)}%</div>
</div>
))}
</div>
)}
</section>
<section className="container mx-auto max-w-6xl px-4 pb-16">
<h2 className="mb-6 text-2xl font-bold">Improve Your Score</h2>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{[
{ label: "Add Bitcoin funds", sub: "Boosts USD balance", href: "/account/add-funds", color: "border-orange-700/40 text-orange-400" },
{ label: "Make a purchase", sub: "Earns LUX credits", href: "/market", color: "border-cyan-700/40 text-cyan-400" },
{ label: "Post in forum", sub: "Grows forum activity", href: "/forum", color: "border-purple-700/40 text-purple-400" },
{ label: "Write a testimonial", sub: "Adds to your score", href: "/testimonials#submit", color: "border-emerald-700/40 text-emerald-400" },
].map((item) => (
<Link
key={item.href}
href={item.href}
className={`rounded-xl border p-5 transition-all hover:bg-white/[0.03] ${item.color}`}
>
<div className="mb-6 flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="h-12 w-12 rounded-full bg-gradient-to-r from-green-700 to-emerald-800 flex items-center justify-center text-xl font-bold">
{review.user.charAt(0)}
</div>
<div>
<div className="font-bold">{review.user}</div>
<div className="text-sm text-gray-400">{review.date}</div>
</div>
</div>
<div className="text-3xl font-bold text-green-400">{review.rating}.0</div>
</div>
<p className="text-gray-300">{review.comment}</p>
<div className="mt-6 flex gap-2">
{Array.from({ length: 5 }).map((_, i) => (
<div
key={i}
className={`h-2 flex-1 rounded-full ${i < Math.floor(review.rating / 2) ? 'bg-green-500' : 'bg-gray-800'}`}
></div>
<div className={`font-bold ${item.color.split(" ")[1]}`}>{item.label}</div>
<p className="mt-1 text-xs text-gray-500">{item.sub}</p>
</Link>
))}
</div>
</section>
<section id="methodology" className="container mx-auto max-w-6xl px-4 pb-16">
<h2 className="mb-6 text-2xl font-bold">FAQ</h2>
<div className="space-y-3 max-w-3xl">
{WHAT_IS.map((item, i) => (
<div key={i} className="rounded-xl border border-gray-800 bg-gray-900/30">
<button
type="button"
onClick={() => setOpenFaq(openFaq === i ? null : i)}
className="flex w-full items-center justify-between px-6 py-4 text-left font-medium hover:text-green-300"
>
{item.q}
<span className={`transition-transform ${openFaq === i ? "rotate-180" : ""}`}></span>
</button>
{openFaq === i && (
<div className="border-t border-gray-800 px-6 py-4 text-sm text-gray-400 leading-relaxed">
{item.a}
</div>
)}
</div>
))}
</div>
</section>
{/* Methodology */}
<section id="methodology" className="container mx-auto max-w-6xl px-4 py-12">
<div className="rounded-2xl bg-gradient-to-br from-gray-900 to-black p-10">
<h2 className="text-3xl font-bold">How We Calculate Trust</h2>
<p className="mt-6 text-gray-300">
Our score is derived from seven independent factors, each weighted based on communityvoted importance:
</p>
<div className="mt-10 grid grid-cols-1 gap-8 md:grid-cols-2">
<div>
<h3 className="text-xl font-bold">1. Transaction Success Rate</h3>
<p className="mt-2 text-gray-400">Percentage of orders that are delivered as described, with no disputes.</p>
</div>
<div>
<h3 className="text-xl font-bold">2. Encryption Audit Score</h3>
<p className="mt-2 text-gray-400">Independent security researchers evaluate the platforms cryptographic implementation.</p>
</div>
<div>
<h3 className="text-xl font-bold">3. User Satisfaction Surveys</h3>
<p className="mt-2 text-gray-400">Anonymous feedback collected from verified buyers via encrypted channels.</p>
</div>
<div>
<h3 className="text-xl font-bold">4. Dispute Resolution Efficiency</h3>
<p className="mt-2 text-gray-400">How fairly and quickly the platform resolves conflicts between buyers and sellers.</p>
</div>
<div>
<h3 className="text-xl font-bold">5. Operational Longevity</h3>
<p className="mt-2 text-gray-400">Markets that survive longer without exitscamming gain higher trust.</p>
</div>
<div>
<h3 className="text-xl font-bold">6. Community Sentiment Analysis</h3>
<p className="mt-2 text-gray-400">Naturallanguage processing of forum discussions and review mentions.</p>
</div>
</div>
<div className="mt-12 rounded-2xl bg-green-900/20 p-8">
<h3 className="text-2xl font-bold">Transparency Note</h3>
<p className="mt-4 text-gray-300">
Scores blend uptime telemetry, dispute volume, and sentiment scrapes weights change when the model is re-tuned. Use this board as a first pass, not the final word on any vendor.
</p>
</div>
</div>
</section>
{/* Footer */}
<footer className="border-t border-gray-800 px-4 py-12">
<div className="container mx-auto max-w-6xl">
<div className="grid grid-cols-1 gap-10 md:grid-cols-3">
<div>
<h3 className="mb-4 text-xl font-bold">DarkNet Trust Score</h3>
<p className="text-gray-400">
An independent, communitydriven reputation platform for darknet marketplaces. Our goal is to reduce fraud and increase transparency.
</p>
</div>
<div>
<h3 className="mb-4 font-bold">LIMITATION</h3>
<p className="text-sm text-gray-500">
Heuristic scores are not legal, financial, or operational guarantees. Wash trading and brigading happen cross-check with escrow receipts you control.
</p>
</div>
<div>
<h3 className="mb-4 font-bold">CONTACT</h3>
<p className="text-gray-400">
Encrypted contact: <span className="text-green-400">trust@darknetscore.example</span>
</p>
<div className="mt-6 flex gap-4">
<button className="rounded-full bg-gray-800 p-3 hover:bg-gray-700">🔒</button>
<button className="rounded-full bg-gray-800 p-3 hover:bg-gray-700">📊</button>
<button className="rounded-full bg-gray-800 p-3 hover:bg-gray-700"></button>
</div>
</div>
</div>
<div className="mt-12 border-t border-gray-800 pt-8 text-center text-sm text-gray-500">
<p>© 2026 DarkNet Trust Score · Signal only verify every claim on-chain or in signed messages</p>
</div>
<footer className="border-t border-gray-800 px-4 py-8 text-center text-sm text-gray-600">
<p>CyberLux Activity Dashboard · All data client-side · No tracking</p>
<div className="mt-2 flex justify-center gap-6">
<Link href="/" className="hover:text-gray-300">Hub</Link>
<Link href="/testimonials" className="hover:text-gray-300">Testimonials</Link>
<Link href="/reviews" className="hover:text-gray-300">Reviews</Link>
</div>
</footer>
</div>

View File

@@ -1,144 +1,657 @@
"use client";
import { useState, useEffect } from "react";
import Link from "next/link";
import { useState, useEffect, useCallback, useRef } from "react";
import { useAccount } from "@/contexts/AccountContext";
export default function AdminDashboard() {
const [btcPrice, setBtcPrice] = useState<string>("64,231.45");
const [traffic, setTraffic] = useState<number>(1240);
const [activeUsers, setActiveUsers] = useState<number>(42);
const [broadcast, setBroadcast] = useState("");
const [terminalLines, setTerminalLines] = useState<string[]>([]);
const [sessionToken, setSessionToken] = useState("");
// ─── Types ───────────────────────────────────────────────────────────────────
useEffect(() => {
setSessionToken(Math.random().toString(36).slice(2, 15));
}, []);
useEffect(() => {
const interval = setInterval(() => {
setBtcPrice((prev) => (parseFloat(prev.replace(',', '')) + (Math.random() - 0.5) * 10).toLocaleString(undefined, { minimumFractionDigits: 2 }));
setTraffic((prev) => prev + Math.floor(Math.random() * 5));
setActiveUsers((prev) => Math.max(10, prev + Math.floor(Math.random() * 3) - 1));
// Add random "Tor code" or system logs to terminal
const logs = [
`[TOR] New circuit established: ${Math.random().toString(16).slice(2, 10)}.onion`,
`[NGINX] 200 GET /market from 127.0.0.1`,
`[SHADOW] Broadcast packet sent to ${Math.floor(Math.random() * 1000)} nodes`,
`[SECURITY] DDoS attempt mitigated from 192.168.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}`,
`[BITCOIN] Block ${Math.floor(Math.random() * 800000)} confirmed`
];
setTerminalLines(prev => [...prev.slice(-15), logs[Math.floor(Math.random() * logs.length)]]);
}, 2000);
return () => clearInterval(interval);
}, []);
const handleBroadcast = () => {
localStorage.setItem("shadow_broadcast", broadcast);
setTerminalLines(prev => [...prev, `[ADMIN] GLOBAL BROADCAST: ${broadcast}`]);
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="min-h-screen bg-[#050000] text-[#00ff41] font-mono p-6 overflow-hidden flex flex-col">
{/* Top Bar */}
<header className="flex justify-between items-center border-b border-[#00ff41]/30 pb-4 mb-6">
<div className="flex items-center gap-4">
<div className="w-3 h-3 bg-red-600 rounded-full animate-ping" />
<h1 className="text-2xl font-black tracking-tighter uppercase text-white">Shadow Control Center</h1>
</div>
<div className="flex gap-8 text-[10px] uppercase tracking-widest">
<div>Uptime: 142:22:01</div>
<div className="text-green-500">Encrypted: AES-256-GCM</div>
</div>
</header>
<div className="grid grid-cols-12 gap-6 flex-1">
{/* Left Column: Stats & Broadcast */}
<div className="col-span-12 lg:col-span-4 space-y-6">
<div className="bg-black border border-[#00ff41]/20 p-6 shadow-[0_0_20px_rgba(0,255,65,0.05)]">
<h2 className="text-xs uppercase opacity-50 mb-4">Network Vitals</h2>
<div className="space-y-4">
<div className="flex justify-between items-end">
<span className="text-[10px] uppercase">BTC/USD</span>
<span className="text-2xl font-bold text-white">${btcPrice}</span>
</div>
<div className="flex justify-between items-end">
<span className="text-[10px] uppercase">Total Nodes</span>
<span className="text-2xl font-bold text-white">12,402</span>
</div>
<div className="flex justify-between items-end">
<span className="text-[10px] uppercase">Active Users</span>
<span className="text-2xl font-bold text-white">{activeUsers}</span>
</div>
</div>
</div>
<div className="bg-black border border-red-600/40 p-6 shadow-[0_0_20px_rgba(255,0,0,0.1)]">
<h2 className="text-xs uppercase text-red-600 mb-4 font-bold">Global Broadcast</h2>
<textarea
value={broadcast}
onChange={(e) => setBroadcast(e.target.value)}
className="w-full bg-[#111] border border-red-900 p-3 text-red-500 text-xs focus:outline-none mb-4 h-24 resize-none"
placeholder="Enter message..."
/>
<button
onClick={handleBroadcast}
className="w-full bg-red-900/90 py-2 text-xs font-bold uppercase text-red-100 transition-all hover:bg-red-800"
>
Execute Broadcast
</button>
</div>
<div className="bg-black border border-[#00ff41]/20 p-6 h-48 overflow-hidden relative">
<h2 className="text-xs uppercase opacity-50 mb-4">Node Map</h2>
<div className="absolute inset-0 opacity-20 pointer-events-none bg-[url('https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExNHJ6Z3R6Z3R6Z3R6Z3R6Z3R6Z3R6Z3R6Z3R6Z3R6Z3R6Z3R6Z3ZpZGUmc2lkPWUmdj0xJm5hbWU9Zw/3o7TKMGpxx8G3Xm8qA/giphy.gif')] bg-cover" />
<div className="relative z-10 text-[8px] space-y-1">
<div>US-EAST-1: ONLINE</div>
<div>EU-WEST-2: ONLINE</div>
<div>ASIA-SOUTH-1: ONLINE</div>
<div className="text-yellow-500">TOR-EXIT-04: LATENCY</div>
</div>
</div>
</div>
{/* Middle Column: Terminal & Live Stream */}
<div className="col-span-12 lg:col-span-8 flex flex-col gap-6">
<div className="flex-1 bg-black border border-[#00ff41]/20 p-4 font-mono text-[10px] overflow-hidden flex flex-col">
<div className="flex justify-between items-center mb-2 border-b border-[#00ff41]/10 pb-2">
<span className="uppercase opacity-50">System Terminal</span>
<span className="text-green-500 animate-pulse"> LIVE</span>
</div>
<div className="flex-1 overflow-y-auto space-y-1 scrollbar-hide">
{terminalLines.map((line, i) => (
<div key={i} className={line.includes('[ADMIN]') ? 'text-red-500 font-bold' : ''}>
{line}
</div>
))}
<div className="animate-pulse">_</div>
</div>
</div>
<div className="h-64 bg-black border border-[#00ff41]/20 relative overflow-hidden">
<div className="absolute top-2 left-2 z-20 bg-black/80 px-2 py-1 text-[8px] uppercase border border-[#00ff41]/20">
Live Feed: Node_05 (Red Room)
</div>
<div className="absolute inset-0 opacity-40 bg-[url('https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExNHJ6Z3R6Z3R6Z3R6Z3R6Z3R6Z3R6Z3R6Z3R6Z3R6Z3R6Z3R6Z3ZpZGUmc2lkPWUmdj0xJm5hbWU9Zw/oEI9uWUqnW3CeEnwL6/giphy.gif')] bg-cover" />
<div className="absolute inset-0 flex items-center justify-center">
<div className="text-center">
<div className="text-2xl font-black text-white/20 tracking-widest uppercase">No Signal</div>
<div className="text-[8px] uppercase opacity-20">Encrypted Stream #8821</div>
</div>
</div>
</div>
</div>
</div>
{/* Bottom Bar */}
<footer className="mt-6 border-t border-[#00ff41]/30 pt-4 flex justify-between items-center text-[8px] uppercase tracking-widest opacity-50">
<div>Shadow Network OS v4.0.1-stable</div>
<div>Current Session: {sessionToken || "—"}</div>
<div className="text-red-500 font-bold">Warning: Unauthorized access is logged</div>
</footer>
<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 [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 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 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 fetchLedger(); void fetchRaffle(); void fetchMessages(); }, 15_000);
return () => clearInterval(id);
}, [user, fetchBtc, fetchLedger, fetchRaffle, fetchMessages]);
// Raffle countdown
useEffect(() => {
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");
};
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 () => {
if (!broadcast.trim()) return;
try {
await fetch("/api/messages", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ from: "System Admin", text: `[BROADCAST] ${broadcast}`, channel: "global" }),
});
log(`[COMMS] Global broadcast sent: "${broadcast}"`);
setBroadcast("");
void fetchMessages();
} 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();
};
const nukeMessages = async () => {
if (!confirm("Nuke all comms?")) return;
await fetch(`/api/messages?all=true`, { method: "DELETE" });
log("[COMMS] All messages purged.");
void fetchMessages();
};
// ── Auth gate ────────────────────────────────────────────────────────────────
if (!hydrated) return (
<div className="flex min-h-screen items-center justify-center bg-[#050000] text-[#00ff66] font-mono text-sm">
Initializing
</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>
);
}

View File

@@ -14,10 +14,10 @@ export default function VendorsHallPage() {
<div className="min-h-screen bg-[#060508] text-[#e8e4dc]">
<div className="border-b border-amber-900/40 bg-gradient-to-r from-[#1a0f08] via-[#120a06] to-[#1a0f08]">
<div className="mx-auto max-w-6xl px-4 py-10">
<p className="font-mono text-[10px] uppercase tracking-[0.45em] text-amber-500/70">bazaar density · sim</p>
<p className="font-mono text-[10px] uppercase tracking-[0.45em] text-amber-500/70">bazaar density</p>
<h1 className="mt-2 font-orbitron text-3xl font-black text-amber-100 md:text-5xl">Vendor hall</h1>
<p className="mt-3 max-w-2xl text-sm text-amber-200/60">
<strong className="text-amber-300">{SHOP_SELLER_COUNT}</strong> fictional stalls fueling{" "}
<strong className="text-amber-300">{SHOP_SELLER_COUNT}</strong> vendor stalls fueling{" "}
<strong className="text-amber-300">{SHOP_PRODUCTS.length}</strong> SKUs scroll the arcade, click a handle for their shelf.
</p>
<div className="mt-6 flex flex-wrap items-center gap-3">

View File

@@ -1,68 +1,202 @@
"use client";
import { useState } from "react";
import Link from "next/link";
import PayWithUsdBalanceButton from "@/components/PayWithUsdBalanceButton";
import ThemedLayout from "@/components/layouts/ThemedLayout";
import { getMerchantBtcAddress, isMerchantBtcConfigured } from "@/lib/merchantBtc";
import { useCart } from "@/contexts/CartContext";
import type { ShopProduct } from "@/lib/shopCatalog";
export default function PayPalMarket() {
const btcAddr = getMerchantBtcAddress();
const btcReady = isMerchantBtcConfigured();
type Pass = {
id: string;
name: string;
description: string;
features: string[];
price: number;
currency: "USD";
tier: "standard" | "elevated" | "handler" | "principal";
luxReward: number;
};
const wallets = [
{ id: 1, balance: "$1,240.00", price: "0.002 BTC", type: "Personal" },
{ id: 2, balance: "$3,500.00", price: "0.005 BTC", type: "Business" },
{ id: 3, balance: "$8,900.00", price: "0.012 BTC", type: "Business Premier" },
{ id: 4, balance: "$12,000.00", price: "0.018 BTC", type: "Corporate" },
const PASSES: Pass[] = [
{
id: "pass_standard",
name: "Standard Pass",
description: "Base access — unlocks buyer profile, order vault, and forum post quota.",
features: ["Buyer account", "Full market access", "Order vault receipts", "Forum posting"],
price: 9.99,
currency: "USD",
tier: "standard",
luxReward: 100,
},
{
id: "pass_elevated",
name: "Elevated Pass",
description: "Enhanced access — priority queue on drops, barter board priority lane, and expanded vault quota.",
features: ["Everything in Standard", "Drop queue priority", "Barter priority lane", "2× LUX on orders", "Expanded vault keys"],
price: 29.99,
currency: "USD",
tier: "elevated",
luxReward: 350,
},
{
id: "pass_handler",
name: "Handler Pass",
description: "Vendor-level access — stall listing, exchange board elevated visibility, direct forum ring IV key.",
features: ["Everything in Elevated", "Vendor stall listing", "Exchange elevated visibility", "Ring IV forum key", "Inner Circle Handler tier"],
price: 74.99,
currency: "USD",
tier: "handler",
luxReward: 1000,
},
{
id: "pass_principal",
name: "Principal Pass",
description: "Maximum tier — all access surfaces unlocked, relay contact, and lore archive key.",
features: ["All access surfaces", "Operator relay contact", "Full lore archive key", "Inner Circle Principal", "Priority dispute resolution"],
price: 199.99,
currency: "USD",
tier: "principal",
luxReward: 3000,
},
];
const TIER_COLORS: Record<Pass["tier"], string> = {
standard: "from-gray-600 to-gray-500",
elevated: "from-cyan-700 to-blue-600",
handler: "from-purple-700 to-purple-500",
principal: "from-amber-600 to-yellow-500",
};
const TIER_BORDER: Record<Pass["tier"], string> = {
standard: "border-gray-600/40",
elevated: "border-cyan-600/40",
handler: "border-purple-600/40",
principal: "border-amber-500/40",
};
export default function DigitalPassesPage() {
const { addToCart } = useCart();
const [added, setAdded] = useState<string | null>(null);
const handleAdd = (pass: Pass) => {
const product: ShopProduct = {
id: pass.id,
name: pass.name,
description: pass.description,
price: pass.price,
currency: pass.currency,
category: "Access Pass",
sellerId: "cyberlux",
};
addToCart(product, 1);
setAdded(pass.id);
setTimeout(() => setAdded(null), 1800);
};
return (
<ThemedLayout theme="retro" title="The Wallet Exchange">
<div className="max-w-4xl mx-auto pt-12 font-mono text-[#0f0]">
<ThemedLayout theme="retro" title="Digital Access Passes">
<div className="mx-auto max-w-5xl px-4 pt-12 pb-20 font-mono">
<header className="border-b-4 border-[#0f0] pb-8 mb-12">
<h1 className="text-5xl font-black uppercase tracking-tighter">The Wallet Exchange</h1>
<p className="text-sm mt-4">Verified PayPal Wallets. Instant Delivery.</p>
<div className="text-[10px] uppercase tracking-widest text-[#0f0]/60 mb-2">CyberLux Store</div>
<h1 className="text-4xl font-black uppercase tracking-tighter text-[#0f0]">Digital Access Passes</h1>
<p className="mt-4 text-sm text-[#0f0]/70 max-w-2xl">
One-time purchases that expand your CyberLux access tier and award permanent LUX credits. Passes are
virtual payment via USD balance (funded by Bitcoin verify) or checkout. No physical delivery.
</p>
<div className="mt-4 flex flex-wrap gap-3">
<Link
href="/account/add-funds"
className="border border-[#0f0] px-4 py-2 text-xs font-bold uppercase text-[#0f0] hover:bg-[#0f0]/10 transition-all"
>
Add Funds First
</Link>
<Link
href="/inner-circle"
className="border border-[#0f0]/40 px-4 py-2 text-xs font-bold uppercase text-[#0f0]/60 hover:border-[#0f0] hover:text-[#0f0] transition-all"
>
Inner Circle
</Link>
</div>
</header>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 mb-12">
{wallets.map(w => (
<div key={w.id} className="border-2 border-[#0f0] p-8 bg-black/40 hover:bg-[#0f0]/10 transition-all">
<div className="flex justify-between items-start mb-6">
<div className="text-3xl font-bold">{w.balance}</div>
<div className="text-[10px] bg-[#0f0] text-black px-2 py-1 font-bold uppercase">{w.type}</div>
<div className="grid grid-cols-1 gap-8 md:grid-cols-2">
{PASSES.map((pass) => {
const isAdded = added === pass.id;
return (
<div
key={pass.id}
className={`border-2 bg-black/40 p-8 transition-all hover:bg-[#0f0]/5 ${TIER_BORDER[pass.tier]}`}
>
<div className="mb-5 flex items-start justify-between gap-3">
<div>
<div
className={`mb-2 inline-block rounded bg-gradient-to-r ${TIER_COLORS[pass.tier]} px-3 py-1 text-[10px] font-black uppercase text-white`}
>
{pass.tier}
</div>
<div className="space-y-2 text-xs opacity-60 mb-8">
<div>{">"} Verified Email: YES</div>
<div>{">"} Linked Bank: YES</div>
<div>{">"} Transaction History: 12+ Months</div>
<h3 className="text-xl font-black uppercase text-[#0f0]">{pass.name}</h3>
</div>
<div className="flex justify-between items-center">
<div className="text-xl font-bold">{w.price}</div>
<button className="bg-[#0f0] text-black px-6 py-2 font-bold uppercase text-xs hover:bg-[#0f0]/80 transition-all">
Buy Now
</button>
<div className="text-right">
<div className="text-2xl font-black text-[#0f0]">${pass.price}</div>
<div className="text-[10px] text-[#0f0]/50">+{pass.luxReward} LUX</div>
</div>
</div>
<p className="mb-5 text-sm text-[#0f0]/70 leading-relaxed">{pass.description}</p>
<ul className="mb-6 space-y-1.5">
{pass.features.map((f) => (
<li key={f} className="flex items-center gap-2 text-xs text-[#0f0]/80">
<span className="text-[#0f0]">{">"}</span>
{f}
</li>
))}
</ul>
<button
type="button"
onClick={() => handleAdd(pass)}
className={`w-full py-3 font-black uppercase text-sm transition-all ${
isAdded
? "bg-[#0f0] text-black"
: "border-2 border-[#0f0] text-[#0f0] hover:bg-[#0f0] hover:text-black"
}`}
>
{isAdded ? "✓ Added to Cart" : `Add to Cart — $${pass.price}`}
</button>
<div className="mt-3 border border-[#0f0]/25 bg-black/60 p-3">
<p className="mb-2 text-[10px] font-bold uppercase text-[#0f0]/70">Or pay instantly (BTC-funded USD)</p>
<PayWithUsdBalanceButton
amountUsd={pass.price}
title={`Pass: ${pass.name}`}
description={`Tier ${pass.tier} · +${pass.luxReward} LUX`}
luxBonus={pass.luxReward}
size="sm"
className="w-full border-[#0f0]/40 !text-[#0f0]"
/>
</div>
</div>
);
})}
</div>
<div className="bg-[#0f0]/10 border-2 border-[#0f0] p-8 mb-12">
<h3 className="text-xl font-bold mb-4 uppercase">Payment Instructions</h3>
<p className="text-xs mb-6 leading-relaxed">
Bitcoin payments use the same merchant address as CyberLux checkout. After the transaction is
confirmed on-chain, verify the txid on the checkout page to add USD to your spendable balance.
</p>
<div className="bg-black text-white p-4 font-bold break-all mb-4 border border-[#0f0] text-[11px] leading-relaxed">
{btcReady ? btcAddr : "[Set NEXT_PUBLIC_MERCHANT_BTC_ADDRESS in .env.local]"}
</div>
<div className="flex items-center gap-4 opacity-50">
<div className="w-10 h-10 border-2 border-[#0f0] flex items-center justify-center font-bold">E</div>
<div className="text-[8px] uppercase">
Escrow Assured<br />by ShadowGuard
</div>
<div className="mt-12 border-4 border-[#0f0]/20 bg-[#001200] p-8 text-sm">
<h3 className="mb-4 font-black uppercase text-[#0f0]">How Passes Work</h3>
<ul className="space-y-2 text-[#0f0]/70 text-xs">
<li>{">"} Passes are digital items checkout with USD balance (Bitcoin-funded) or other accepted payment.</li>
<li>{">"} On purchase, LUX credits are applied to your handle and tier is updated in the Inner Circle.</li>
<li>{">"} All state is client-side. Export your account bundle from /dashboard to back it up.</li>
<li>{">"} No refunds on digital passes once LUX has been applied to your account.</li>
</ul>
<div className="mt-6 flex gap-4">
<Link href="/checkout" className="text-xs text-[#0f0] hover:underline">Go to Checkout </Link>
<Link href="/dashboard" className="text-xs text-[#0f0]/60 hover:text-[#0f0]">Your Dashboard</Link>
</div>
</div>
<div className="text-center text-[10px] opacity-40 uppercase tracking-widest">
Node 24 // Shadow Network // Est. 2024
<div className="mt-8 text-center text-[10px] text-[#0f0]/30 uppercase tracking-widest">
CyberLux Digital Passes · Client-side entitlement system · Est. 2024
</div>
</div>
</ThemedLayout>

View File

@@ -1,171 +1,40 @@
"use client";
import Link from "next/link";
import { useState } from "react";
/** Hub CTA — real provisioning lives on /sign-up (local vault + per-handle balance). */
const AccountCreation = () => {
const [step, setStep] = useState(1);
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [recoveryPhrase, setRecoveryPhrase] = useState("");
const [encryptionKey, setEncryptionKey] = useState("");
const [generated, setGenerated] = useState(false);
const generateRecovery = () => {
const phrases = [
"phantom quantum glacier nexus",
"crimson velvet paradox silence",
"neon abyss whisper eclipse",
"zero trace ghost protocol",
];
const randomPhrase = phrases[Math.floor(Math.random() * phrases.length)];
setRecoveryPhrase(randomPhrase);
setEncryptionKey(btoa(Date.now().toString()).slice(0, 16));
setGenerated(true);
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (step < 3) {
setStep(step + 1);
} else {
alert("Identity provisioned. Keys derived locally — backup your recovery phrase offline.");
}
};
return (
<div className="glass mx-auto max-w-2xl rounded-3xl border border-white/10 p-10">
<h2 className="mb-8 font-orbitron text-4xl font-bold text-center">CREATE SHADOW IDENTITY</h2>
{/* Steps */}
<div className="mb-10 flex justify-between">
{[1, 2, 3].map((s) => (
<div key={s} className="flex flex-col items-center">
<div
className={`flex h-12 w-12 items-center justify-center rounded-full border-2 text-lg font-bold ${step >= s
? "border-neon-cyan bg-neon-cyan/20 text-neon-cyan"
: "border-white/20 text-foreground/40"
}`}
>
{s}
</div>
<div className="mt-2 text-sm">
{s === 1 && "Credentials"}
{s === 2 && "Recovery"}
{s === 3 && "Encryption"}
</div>
</div>
))}
</div>
<form onSubmit={handleSubmit}>
{step === 1 && (
<div className="space-y-6">
<div>
<label className="mb-2 block font-medium">Shadow Alias</label>
<input
type="text"
className="glass w-full rounded-xl border border-white/10 bg-transparent p-4"
placeholder="e.g., Ghost_23"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
<p className="mt-2 text-sm text-foreground/60">Never your real name. This alias is encrypted.</p>
</div>
<div>
<label className="mb-2 block font-medium">ZeroKnowledge Password</label>
<input
type="password"
className="glass w-full rounded-xl border border-white/10 bg-transparent p-4"
placeholder="••••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
<p className="mt-2 text-sm text-foreground/60">We never store your password. Its used to derive your encryption key.</p>
</div>
</div>
)}
{step === 2 && (
<div className="space-y-6">
<div>
<label className="mb-2 block font-medium">Recovery Phrase</label>
<div className="glass rounded-xl border border-neon-cyan/30 p-6 font-mono text-center text-lg">
{generated ? recoveryPhrase : "Click generate to create"}
</div>
<button
type="button"
className="mt-4 w-full rounded-full bg-gradient-to-r from-neon-cyan to-neon-purple py-3 font-bold text-background"
onClick={generateRecovery}
>
GENERATE RECOVERY PHRASE
</button>
<p className="mt-4 text-sm text-foreground/60">
Write this down physically. Its the only way to recover your account. We do not store it.
</p>
</div>
</div>
)}
{step === 3 && (
<div className="space-y-6">
<div>
<label className="mb-2 block font-medium">Encryption Key</label>
<div className="glass rounded-xl border border-neon-green/30 p-6 font-mono text-center text-lg">
{encryptionKey || "Not generated"}
</div>
<p className="mt-4 text-sm text-foreground/60">
This key encrypts all your data locally. It is derived from your password and never leaves your device.
</p>
</div>
<div className="rounded-xl bg-gradient-to-r from-neon-cyan/10 to-neon-purple/10 p-6">
<h3 className="font-bold">Identity Summary</h3>
<div className="mt-4 grid grid-cols-2 gap-4 text-sm">
<div>Alias</div>
<div className="font-mono">{username || "—"}</div>
<div>Recovery Phrase</div>
<div className="font-mono truncate">{recoveryPhrase || "—"}</div>
<div>Encryption</div>
<div className="font-mono text-neon-green">AES256GCM</div>
</div>
</div>
</div>
)}
<div className="mt-10 flex justify-between">
<button
type="button"
className="glass rounded-full px-8 py-3 font-medium disabled:opacity-30"
onClick={() => setStep(step - 1)}
disabled={step === 1}
>
Back
</button>
<button
type="submit"
className="rounded-full bg-gradient-to-r from-neon-cyan to-neon-purple px-10 py-3 font-bold text-background"
>
{step === 3 ? "COMPLETE IDENTITY CREATION" : "CONTINUE →"}
</button>
</div>
</form>
<div className="mt-10 border-t border-white/10 pt-8 text-center text-sm text-foreground/40">
<p>Keys and aliases never leave this browser profile. Clear storage = identity loss.</p>
<p className="mt-4 text-foreground/55">
For a <strong className="text-neon-cyan/90">live stall session</strong> tied to wallet and activity feeds, use{" "}
<Link href="/sign-up" className="text-neon-cyan underline hover:text-neon-purple">
create account
<h2 className="mb-4 text-center font-orbitron text-3xl font-bold md:text-4xl">Create an account</h2>
<p className="text-center text-foreground/70">
One handle for forum, exchange, barter, checkout, vault, and Bitcoin-funded USD balance on this origin.
Credentials stay in your browser; use{" "}
<Link href="/account/hidden-services" className="text-neon-cyan underline">
Hidden services
</Link>{" "}
or{" "}
<Link href="/sign-in" className="text-neon-cyan underline hover:text-neon-purple">
sign in
</Link>
.
to copy your identity to another .onion host.
</p>
<div className="mt-10 flex flex-col items-center justify-center gap-4 sm:flex-row">
<Link
href="/sign-up"
className="inline-block rounded-full bg-gradient-to-r from-neon-cyan to-neon-purple px-10 py-4 text-center font-bold text-background"
>
Register
</Link>
<Link
href="/sign-in"
className="inline-block rounded-full border border-white/20 px-10 py-4 text-center font-medium hover:border-neon-cyan/50"
>
Sign in
</Link>
</div>
<p className="mt-8 text-center text-sm text-foreground/50">
Add funds after sign-in:{" "}
<Link href="/account/add-funds" className="text-neon-green underline">
/account/add-funds
</Link>
</p>
</div>
);
};

View File

@@ -5,13 +5,13 @@ import { AccountProvider } from "@/contexts/AccountContext";
import { CartProvider } from "@/contexts/CartContext";
import { WalletProvider } from "@/contexts/WalletContext";
/** Client-only wrappers so wallet + persistent account work on every route. */
/** Account must wrap wallet so balance + vault ledger are keyed by the signed-in handle. */
export default function AppProviders({ children }: { children: ReactNode }) {
return (
<WalletProvider>
<AccountProvider>
<WalletProvider>
<CartProvider>{children}</CartProvider>
</AccountProvider>
</WalletProvider>
</AccountProvider>
);
}

View File

@@ -1,156 +1,174 @@
"use client";
import { useState, useEffect, useRef } from "react";
import { useState, useEffect, useRef, useCallback } from "react";
import Link from "next/link";
import { useAccount } from "@/contexts/AccountContext";
const ChatWidget = () => {
const [messages, setMessages] = useState([
{ id: 1, sender: "System", text: "Welcome to the encrypted channel. All messages are endtoend encrypted.", time: "12:00" },
{ id: 2, sender: "Ghost", text: "Has anyone tested the new stimulant batch?", time: "12:05" },
{ id: 3, sender: "Vendor_X", text: "Batch #8 passes all purity tests. Available for bulk.", time: "12:07" },
{ id: 4, sender: "Anonymous", text: "Need a EU passport within 48 hours. DM if you can deliver.", time: "12:10" },
]);
type Msg = {
id: string;
from: string;
text: string;
ts: number;
};
function now(ts: number) {
return new Date(ts).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
}
export default function ChatWidget() {
const { user } = useAccount();
const handle = user?.displayName ?? user?.username ?? "anon";
const [messages, setMessages] = useState<Msg[]>([]);
const [input, setInput] = useState("");
/** Set after mount so SSR HTML matches first client paint (no Math.random in initial state). */
const [encryptionKey, setEncryptionKey] = useState("");
const chatContainerRef = useRef<HTMLDivElement>(null);
const [hydrated, setHydrated] = useState(false);
const endRef = useRef<HTMLDivElement>(null);
useEffect(() => {
setEncryptionKey("session_" + Math.random().toString(36).substring(2, 9));
const fetchMessages = useCallback(async () => {
try {
const res = await fetch("/api/messages?channel=hub");
const data = await res.json();
if (data.ok && data.messages) {
setMessages(data.messages);
}
} catch (e) {
// ignore
}
}, []);
useEffect(() => {
if (chatContainerRef.current) {
chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight;
}
}, [messages]);
fetchMessages();
setHydrated(true);
const interval = setInterval(fetchMessages, 3000);
return () => clearInterval(interval);
}, [fetchMessages]);
const handleSend = () => {
if (!input.trim()) return;
const newMessage = {
id: messages.length + 1,
sender: "You",
text: input,
time: new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }),
};
setMessages([...messages, newMessage]);
useEffect(() => {
if (hydrated && messages.length > 0) {
endRef.current?.scrollIntoView({ behavior: "smooth" });
}
}, [messages, hydrated]);
const send = async () => {
const text = input.trim();
if (!text) return;
setInput("");
// Simulate a reply after a delay
setTimeout(() => {
const replies = [
"Received. Encryption verified.",
"I can help with that. Check your vault.",
"New drop incoming in 24 hours.",
"Your request has been logged.",
];
const randomReply = replies[Math.floor(Math.random() * replies.length)];
const replyMessage = {
id: messages.length + 2,
sender: "System",
text: randomReply,
time: new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }),
};
setMessages(prev => [...prev, replyMessage]);
}, 1000);
};
// Optimistic update
const optimisticMsg: Msg = { id: "temp-" + Date.now(), from: handle, text, ts: Date.now() };
setMessages((p) => [...p, optimisticMsg]);
const handleEncrypt = () => {
alert(`Session key: ${encryptionKey}\nAll messages are encrypted with this key.`);
try {
await fetch("/api/messages", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ from: handle, text, channel: "hub" }),
});
fetchMessages();
} catch (e) {
// ignore
}
};
return (
<div className="glass rounded-3xl border border-white/10 p-8">
<div className="mb-8 flex items-center justify-between">
<div className="mb-6 flex flex-wrap items-start justify-between gap-4">
<div>
<h2 className="font-orbitron text-3xl font-bold">ENCRYPTED CHAT</h2>
<p className="text-foreground/60">Endtoend encrypted messaging. No logs, no traces.</p>
<h2 className="font-orbitron text-3xl font-bold">CHANNEL CHAT</h2>
<p className="mt-1 text-sm text-foreground/60">
Global hub channel synced live.{" "}
{user ? (
<span className="text-neon-cyan">Posting as @{user.username}</span>
) : (
<Link href="/sign-in" className="text-neon-cyan underline">
Sign in to tag your handle
</Link>
)}
</p>
</div>
<div className="flex gap-3 text-[10px] font-mono uppercase tracking-wider text-foreground/40">
<span className="flex items-center gap-1">
<span className="inline-block h-2 w-2 animate-pulse rounded-full bg-neon-green" />
channel active
</span>
<span>live sync</span>
</div>
<button
onClick={handleEncrypt}
className="rounded-full bg-gradient-to-r from-neon-cyan to-neon-purple px-6 py-3 font-bold text-background"
>
🔐 SHOW SESSION KEY
</button>
</div>
{/* Chat messages */}
<div
ref={chatContainerRef}
className="glass mb-6 h-96 overflow-y-auto rounded-2xl border border-white/10 p-6"
<div className="mb-4 h-72 overflow-y-auto rounded-2xl border border-white/10 bg-black/30 p-4 space-y-3">
{messages.length === 0 ? (
<div className="flex h-full items-center justify-center text-sm text-foreground/40">No messages yet. Be the first!</div>
) : (
messages.map((msg) => {
const mine = msg.from === handle || (msg.from === "anon" && !user);
return (
<div key={msg.id} className={`flex flex-col ${mine ? "items-end" : "items-start"}`}>
<div className="mb-0.5 flex items-center gap-2">
<span
className={`text-xs font-medium ${
mine
? "text-neon-cyan"
: msg.from === "System"
? "text-neon-purple"
: "text-foreground/70"
}`}
>
{messages.map((msg) => (
<div
key={msg.id}
className={`mb-4 ${msg.sender === "You" ? "text-right" : ""}`}
>
<div className="mb-1 flex items-center gap-2">
<span className={`inline-block rounded-full px-3 py-1 text-xs font-medium ${msg.sender === "You"
? "bg-neon-cyan/20 text-neon-cyan"
: msg.sender === "System"
? "bg-neon-purple/20 text-neon-purple"
: "bg-white/10"
}`}>
{msg.sender}
{mine ? `@${handle}` : msg.from === "System" ? "⚡ System" : `@${msg.from}`}
</span>
<span className="text-xs text-foreground/40">{msg.time}</span>
<span className="text-[10px] text-foreground/30">{now(msg.ts)}</span>
</div>
<div className={`glass inline-block max-w-[80%] rounded-2xl px-4 py-3 ${msg.sender === "You"
? "bg-gradient-to-r from-neon-cyan/20 to-neon-cyan/10"
: "bg-white/5"
}`}>
<div
className={`max-w-[80%] rounded-2xl px-4 py-2 text-sm ${
mine
? "bg-neon-cyan/15 text-neon-cyan"
: msg.from === "System"
? "bg-neon-purple/10 text-neon-purple/90"
: "bg-white/5 text-foreground/90"
}`}
>
{msg.text}
</div>
</div>
))}
);
})
)}
<div ref={endRef} />
</div>
{/* Input area */}
<div className="flex gap-4">
<div className="flex gap-3">
<input
type="text"
className="glass flex-1 rounded-2xl border border-white/10 bg-transparent px-6 py-4"
placeholder="Type your encrypted message..."
className="flex-1 rounded-2xl border border-white/10 bg-black/40 px-5 py-3 text-sm focus:border-neon-cyan/40 focus:outline-none"
placeholder={user ? `Message as @${user.username}` : "Message the channel…"}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleSend()}
onKeyDown={(e) => e.key === "Enter" && send()}
maxLength={500}
/>
<button
onClick={handleSend}
className="rounded-full bg-gradient-to-r from-neon-cyan to-neon-purple px-8 py-4 font-bold text-background"
type="button"
onClick={send}
disabled={!input.trim()}
className="rounded-2xl bg-gradient-to-r from-neon-cyan to-neon-purple px-6 py-3 text-sm font-bold text-background disabled:opacity-40"
>
SEND
Send
</button>
</div>
{/* Chat features */}
<div className="mt-8 grid grid-cols-2 gap-6 md:grid-cols-4">
<div className="glass rounded-2xl border border-white/10 p-4 text-center">
<div className="text-2xl">🔒</div>
<div className="mt-2 text-sm font-bold">AES256GCM</div>
<div className="text-xs text-foreground/60">Encryption</div>
<div className="mt-6 grid grid-cols-2 gap-4 md:grid-cols-4">
{[
{ icon: "🌍", label: "Global Sync", sub: "Talk to everyone" },
{ icon: "💬", label: "Open channel", sub: "Hub-wide thread" },
{ icon: "📬", label: "Forum threads", sub: "/forum for persistence" },
{ icon: "📦", label: "Market drops", sub: "/drops for schedule" },
].map((f) => (
<div key={f.label} className="glass rounded-2xl border border-white/10 p-4 text-center">
<div className="text-2xl">{f.icon}</div>
<div className="mt-2 text-sm font-bold">{f.label}</div>
<div className="text-xs text-foreground/50">{f.sub}</div>
</div>
<div className="glass rounded-2xl border border-white/10 p-4 text-center">
<div className="text-2xl"></div>
<div className="mt-2 text-sm font-bold">ZeroKnowledge</div>
<div className="text-xs text-foreground/60">No server storage</div>
</div>
<div className="glass rounded-2xl border border-white/10 p-4 text-center">
<div className="text-2xl">🌐</div>
<div className="mt-2 text-sm font-bold">WebRTC</div>
<div className="text-xs text-foreground/60">P2P possible</div>
</div>
<div className="glass rounded-2xl border border-white/10 p-4 text-center">
<div className="text-2xl">🕶</div>
<div className="mt-2 text-sm font-bold">SelfDestruct</div>
<div className="text-xs text-foreground/60">Messages expire</div>
</div>
</div>
<div className="mt-8 text-center text-xs text-foreground/40">
<p>Ephemeral client-side queue nothing leaves this browser profile until a real transport is configured.</p>
))}
</div>
</div>
);
};
export default ChatWidget;
}

View File

@@ -2,6 +2,7 @@
import Link from "next/link";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useAccount } from "@/contexts/AccountContext";
import { useCart } from "@/contexts/CartContext";
import { useWallet } from "@/contexts/WalletContext";
import { getMerchantBtcAddress, isMerchantBtcConfigured } from "@/lib/merchantBtc";
@@ -17,7 +18,6 @@ const CheckoutFlow = () => {
const { lines, setLineQty, removeLine, clearCart, hydrated: cartHydrated } = useCart();
const [step, setStep] = useState(1);
const [paymentMethod, setPaymentMethod] = useState<"crypto" | "guest" | "card">("crypto");
const [cryptoType, setCryptoType] = useState<"BTC" | "ETH" | "XMR">("BTC");
const [guestEmail, setGuestEmail] = useState("");
const [encryptionLevel, setEncryptionLevel] = useState<"standard" | "enhanced" | "quantum">("enhanced");
const [btcUsd, setBtcUsd] = useState<number | null>(null);
@@ -26,8 +26,10 @@ const CheckoutFlow = () => {
const [verifyBusy, setVerifyBusy] = useState(false);
const [payError, setPayError] = useState<string | null>(null);
const [orderComplete, setOrderComplete] = useState(false);
const [completedOrderId, setCompletedOrderId] = useState<string | null>(null);
const { usdStoreCredit, spendUsdStoreCredit, verifyBtcDeposit } = useWallet();
const { user } = useAccount();
const { usdStoreCredit, spendUsdStoreCredit, verifyBtcDeposit, earnLuxCredits, addVaultReceipt } = useWallet();
useEffect(() => {
let cancelled = false;
@@ -107,14 +109,34 @@ const CheckoutFlow = () => {
const tryCompleteOrder = () => {
setPayError(null);
if (paymentMethod === "crypto") {
if (!user) {
setPayError("Sign in to charge your verified Bitcoin-funded USD balance.");
return;
}
if (!spendUsdStoreCredit(orderTotalUsd)) {
setPayError(
`Insufficient USD balance. You need $${orderTotalUsd.toFixed(2)}. Send Bitcoin to the address below, wait for at least one confirmation, then paste the transaction ID and click Verify.`,
`Insufficient USD balance. You need $${orderTotalUsd.toFixed(2)}. Add funds under Account → Add funds, then verify your txid.`,
);
return;
}
}
const oid =
typeof crypto !== "undefined" && "randomUUID" in crypto
? `CYBR-${crypto.randomUUID().slice(0, 8).toUpperCase()}`
: `CYBR-${Date.now().toString(36).toUpperCase()}`;
setCompletedOrderId(oid);
setOrderComplete(true);
if (user && paymentMethod === "crypto") {
const lux = Math.min(500, Math.max(25, Math.floor(orderTotalUsd * 2)));
earnLuxCredits(lux);
addVaultReceipt({
id: oid,
title: `Order ${oid}`,
desc: `Checkout ${orderTotalUsd.toFixed(2)} USD · ${encryptionLevel} tier.`,
date: new Date().toISOString().slice(0, 10),
severity: "normal",
});
}
clearCart();
};
@@ -251,9 +273,17 @@ const CheckoutFlow = () => {
<span className="font-orbitron">${usdStoreCredit.toFixed(2)}</span>
<span className="text-foreground/60">
{" "}
use after Bitcoin deposit is verified (on-chain).
per signed-in handle after verified Bitcoin deposits.
</span>
</div>
{paymentMethod === "crypto" && !user ? (
<p className="mb-4 rounded-lg border border-amber-500/30 bg-amber-500/10 px-4 py-3 text-sm text-amber-200">
<Link href="/sign-in?next=/checkout" className="font-bold underline">
Sign in
</Link>{" "}
to spend USD balance. You can still browse deposit instructions below.
</p>
) : null}
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
<button
type="button"
@@ -273,7 +303,7 @@ const CheckoutFlow = () => {
>
<div className="mb-4 text-4xl">👤</div>
<div className="font-bold">Guest Checkout</div>
<div className="mt-2 text-sm text-foreground/60">Demo no USD charge</div>
<div className="mt-2 text-sm text-foreground/60">No USD charge order completes locally only</div>
</button>
<button
type="button"
@@ -282,37 +312,14 @@ const CheckoutFlow = () => {
onClick={() => setPaymentMethod("card")}
>
<div className="mb-4 text-4xl">💳</div>
<div className="font-bold">Card (Secure)</div>
<div className="mt-2 text-sm text-foreground/60">Demo no USD charge</div>
<div className="font-bold">Card</div>
<div className="mt-2 text-sm text-foreground/60">Not enabled use Bitcoin balance instead</div>
</button>
</div>
{paymentMethod === "crypto" && (
<div className="mt-8">
<label className="mb-2 block font-medium">Select Cryptocurrency</label>
<div className="flex gap-4">
{(["BTC", "ETH", "XMR"] as const).map((coin) => (
<button
key={coin}
type="button"
className={`glass flex-1 rounded-xl border py-4 ${cryptoType === coin ? "border-neon-cyan bg-neon-cyan/10" : "border-white/10"
}`}
onClick={() => setCryptoType(coin)}
>
<div className="text-2xl">{coin === "BTC" ? "₿" : coin === "ETH" ? "Ξ" : "⏣"}</div>
<div className="font-bold">{coin}</div>
</button>
))}
</div>
{cryptoType !== "BTC" && (
<p className="mt-4 rounded-lg bg-white/5 p-4 text-sm text-foreground/70">
Deposits that add USD balance use <strong>Bitcoin</strong> only. Switch to BTC to see the
deposit address. (ETH/XMR shown for display.)
</p>
)}
{cryptoType === "BTC" && (
<div className="mt-6 space-y-4 rounded-xl bg-white/5 p-4">
<div className="text-sm text-foreground/60">Estimated order total after encryption step</div>
<div className="mt-8 space-y-4 rounded-xl bg-white/5 p-4">
<div className="text-sm text-foreground/60">Bitcoin-only · estimated order total after encryption step</div>
<div className="font-orbitron text-2xl text-neon-cyan">
${orderTotalUsd.toLocaleString()} USD
{orderTotalBtc != null && btcUsd != null && (
@@ -322,14 +329,14 @@ const CheckoutFlow = () => {
)}
</div>
<p className="text-xs text-foreground/50">
Final total includes the encryption add-on you pick in the next step. Fund your balance
with <strong>any amount</strong> of BTC; verified value is credited in USD at spot rate.
You can pay this order once your balance covers the total.
Final total includes the encryption add-on you pick in the next step. Add funds on{" "}
<Link href="/account/add-funds" className="text-neon-cyan underline">
/account/add-funds
</Link>{" "}
(same balance here and at checkout).
</p>
</div>
)}
</div>
)}
{paymentMethod === "guest" && (
<div className="mt-8">
@@ -342,13 +349,13 @@ const CheckoutFlow = () => {
onChange={(e) => setGuestEmail(e.target.value)}
/>
<p className="mt-2 text-sm text-foreground/60">
Guest checkout is a demo and does not charge your USD balance.
Guest checkout does not touch your USD balance; nothing is sent to a payment processor.
</p>
</div>
)}
<div className="mt-10 border-t border-white/10 pt-8">
<h4 className="mb-4 font-orbitron text-lg font-bold text-neon-cyan">Bitcoin deposit (all checkouts)</h4>
<h4 className="mb-4 font-orbitron text-lg font-bold text-neon-cyan">Bitcoin deposit · processor</h4>
{!merchantReady ? (
<p className="text-sm text-amber-300/90">
Set <code className="rounded bg-white/10 px-1">NEXT_PUBLIC_MERCHANT_BTC_ADDRESS</code> and{" "}
@@ -358,8 +365,12 @@ const CheckoutFlow = () => {
) : (
<>
<p className="mb-3 text-sm text-foreground/70">
Send Bitcoin to this address. After the network confirms your payment (at least one
block), paste the transaction ID to add the equivalent USD to your balance.
Send Bitcoin to this address. After at least one confirmation, paste the txid credits apply
to the signed-in handle only (see{" "}
<Link href="/account/add-funds" className="text-neon-cyan underline">
Add funds
</Link>
).
</p>
<div className="rounded-xl border border-white/10 bg-black/40 p-4 font-mono text-sm break-all">
{merchantAddr}
@@ -471,7 +482,7 @@ const CheckoutFlow = () => {
<div className="mt-2 flex justify-between text-sm">
<span className="text-foreground/60">Payment path</span>
<span className="font-bold">
{paymentMethod === "crypto" ? "Crypto (USD balance)" : "Demo (guest/card)"}
{paymentMethod === "crypto" ? "Bitcoin-funded USD balance" : "Guest / card (local only)"}
</span>
</div>
</div>
@@ -481,7 +492,7 @@ const CheckoutFlow = () => {
onClick={tryCompleteOrder}
className="rounded-full bg-gradient-to-r from-neon-cyan to-neon-purple px-10 py-3 font-bold text-background"
>
{paymentMethod === "crypto" ? "Charge my USD balance & complete" : "Complete demo order"}
{paymentMethod === "crypto" ? "Charge my USD balance & complete" : "Complete order (local)"}
</button>
{paymentMethod === "crypto" && (
<p className="mx-auto mt-6 max-w-xl text-xs text-foreground/50">
@@ -506,7 +517,9 @@ const CheckoutFlow = () => {
</p>
<div className="my-10">
<div className="glass mx-auto max-w-md rounded-2xl border border-white/10 p-6">
<div className="font-orbitron text-2xl font-bold text-neon-cyan">ORDER #CYBR9A2FB8E1</div>
<div className="font-orbitron text-2xl font-bold text-neon-cyan">
ORDER #{completedOrderId ?? "—"}
</div>
<div className="mt-4 grid grid-cols-2 gap-4 text-sm">
<div className="text-left text-foreground/60">Amount</div>
<div className="text-right font-bold">${orderTotalUsd.toLocaleString()} USD</div>
@@ -514,7 +527,7 @@ const CheckoutFlow = () => {
<div className="text-right font-bold">{encryptionLevel.toUpperCase()}</div>
<div className="text-left text-foreground/60">Payment</div>
<div className="truncate text-right font-mono text-neon-green">
{paymentMethod === "crypto" ? "USD balance (BTC-funded)" : "Demo guest/card"}
{paymentMethod === "crypto" ? "USD balance (BTC-funded)" : "Guest or card (no processor)"}
</div>
</div>
</div>

View File

@@ -1,100 +0,0 @@
"use client";
import { useState, useEffect, useRef } from "react";
export default function DDoSProtection({ onComplete }: { onComplete: () => void }) {
const [progress, setProgress] = useState(0);
const [status, setStatus] = useState("Checking your browser…");
/** Client-only — avoids SSR/client HTML mismatch from Math.random() in JSX. */
const [rayId, setRayId] = useState("");
const doneRef = useRef(false);
const onCompleteRef = useRef(onComplete);
onCompleteRef.current = onComplete;
useEffect(() => {
setRayId(Math.random().toString(36).substring(2, 10).toUpperCase());
}, []);
useEffect(() => {
const interval = setInterval(() => {
setProgress((prev) => {
if (prev >= 100) {
return 100;
}
const next = prev + Math.random() * 15;
if (next >= 100) {
if (!doneRef.current) {
doneRef.current = true;
clearInterval(interval);
setTimeout(() => onCompleteRef.current(), 500);
}
return 100;
}
return next;
});
}, 400);
const statusUpdates = [
"Checking your browser…",
"Verifying client challenge…",
"Analyzing request signature…",
"Negotiating TLS + Tor circuit…",
"Establishing encrypted tunnel…",
"Access granted.",
];
let statusIndex = 0;
const statusInterval = setInterval(() => {
if (statusIndex < statusUpdates.length - 1) {
statusIndex++;
setStatus(statusUpdates[statusIndex]);
} else {
clearInterval(statusInterval);
}
}, 800);
return () => {
clearInterval(interval);
clearInterval(statusInterval);
};
}, []);
return (
<div
className="fixed inset-0 z-[10000] flex flex-col items-center justify-center bg-black p-8 font-mono text-[#00ff41]"
style={{ backgroundColor: "#000000", color: "#00ff41" }}
>
<div
className="w-full max-w-md border-2 border-[#00ff41]/20 bg-[#050505] p-12 shadow-[0_0_50px_rgba(0,255,65,0.1)]"
style={{ backgroundColor: "#050505", borderColor: "rgba(0,255,65,0.2)" }}
>
<div className="text-4xl mb-8 animate-pulse">🛡</div>
<h1 className="text-2xl font-bold mb-2 uppercase tracking-tighter">DDoS Protection</h1>
<p className="text-xs opacity-60 mb-8 leading-relaxed">
Origin shield active wait while we validate your session. Automated requests are throttled at the edge.
</p>
<div className="w-full h-2 bg-[#00ff41]/10 mb-4 overflow-hidden">
<div
className="h-full bg-[#00ff41] transition-all duration-300 ease-out"
style={{ width: `${progress}%` }}
/>
</div>
<div className="flex justify-between text-[10px] uppercase tracking-widest">
<span>{status}</span>
<span>{Math.floor(progress)}%</span>
</div>
<div className="mt-12 text-[8px] opacity-20 leading-relaxed">
Ray ID: {rayId || "—"}
<br />
Performance & Security by ShadowGuard
</div>
</div>
<p className="pointer-events-none fixed bottom-6 left-0 right-0 px-4 text-center text-2xl font-black text-red-500 sm:text-3xl md:text-4xl">
This site is fake
</p>
</div>
);
}

View File

@@ -0,0 +1,42 @@
"use client";
import Link from "next/link";
/** Global footer: dark-web deployment CTA (replaces old disclosure strip). */
export default function DarkWebLaunchFooter() {
return (
<footer
className="relative z-[1] border-t border-neon-cyan/30 bg-gradient-to-b from-[#0a0a0a] to-black px-4 py-14 shadow-[0_-12px_40px_rgba(0,255,255,0.06)]"
role="contentinfo"
aria-label="Tor deployment"
>
<div className="mx-auto max-w-4xl text-center">
<p className="font-mono text-[10px] uppercase tracking-[0.4em] text-neon-cyan/80">onion · v3 · air-gapped backend</p>
<h2 className="mt-4 font-orbitron text-2xl font-black tracking-tight text-white sm:text-3xl md:text-4xl">
Get this launched on the dark web
</h2>
<p className="mx-auto mt-4 max-w-2xl text-sm text-foreground/70">
One Next.js stack behind nginx loopbacks Tor hidden services per vertical. Bind to localhost, route through Tor
Browser, rotate keys from backups. No clearnet required.
</p>
<div className="mt-8 flex flex-wrap items-center justify-center gap-4">
<Link
href="/launch"
className="rounded-full bg-gradient-to-r from-neon-cyan to-neon-purple px-8 py-3 text-sm font-bold text-black transition hover:opacity-95"
>
Full Tor launch playbook
</Link>
<Link
href="/account/hidden-services"
className="glass rounded-full border border-white/15 px-6 py-3 text-sm font-semibold text-neon-green hover:border-neon-green/50"
>
Mirror list & hostnames
</Link>
</div>
<p className="mt-10 text-xs text-foreground/40">
Operational security is yours. Verify signatures, firewall egress, and jurisdiction before you ship.
</p>
</div>
</footer>
);
}

View File

@@ -0,0 +1,85 @@
"use client";
import Link from "next/link";
import { useWallet } from "@/contexts/WalletContext";
import { useAccount } from "@/contexts/AccountContext";
import { isMerchantBtcConfigured } from "@/lib/merchantBtc";
type DepositWidgetProps = {
compact?: boolean;
};
export default function DepositWidget({ compact = false }: DepositWidgetProps) {
const { usdStoreCredit, luxCredits } = useWallet();
const { user } = useAccount();
const btcReady = isMerchantBtcConfigured();
if (compact) {
return (
<div className="glass rounded-2xl border border-neon-cyan/20 p-5">
<div className="flex items-center justify-between gap-4">
<div>
<div className="text-xs text-foreground/50">Your balance</div>
<div className="mt-0.5 font-orbitron text-2xl text-neon-cyan">
${usdStoreCredit.toFixed(2)}
<span className="ml-2 text-sm text-foreground/40">USD</span>
</div>
<div className="mt-0.5 text-xs text-neon-green">{luxCredits.toLocaleString()} LUX</div>
</div>
<Link
href="/account/add-funds"
className="shrink-0 rounded-xl bg-gradient-to-r from-neon-cyan to-neon-purple px-5 py-3 text-sm font-bold text-background hover:opacity-90"
>
Deposit crypto
</Link>
</div>
{!user && (
<p className="mt-3 text-xs text-amber-300/70">
<Link href="/sign-in?next=/account/add-funds" className="underline">Sign in</Link> to link deposits to your handle.
</p>
)}
{!btcReady && (
<p className="mt-2 text-[10px] text-amber-400/60">
Operator: set <code>NEXT_PUBLIC_MERCHANT_BTC_ADDRESS</code> in .env.local to enable deposits.
</p>
)}
</div>
);
}
return (
<div className="glass rounded-2xl border border-white/10 p-6">
<div className="mb-4 flex items-start justify-between gap-3">
<div>
<div className="text-xs text-foreground/50">Account balance</div>
<div className="mt-1 font-orbitron text-3xl text-neon-cyan">${usdStoreCredit.toFixed(2)}</div>
<div className="mt-0.5 text-sm text-neon-green">{luxCredits.toLocaleString()} LUX</div>
</div>
<Link
href="/account/add-funds"
className="rounded-xl bg-gradient-to-r from-neon-cyan to-neon-purple px-5 py-3 text-sm font-bold text-background hover:opacity-90"
>
Add crypto
</Link>
</div>
<div className="mt-4 rounded-xl border border-white/10 bg-black/30 p-4 text-sm">
<div className="mb-2 text-xs font-bold uppercase tracking-wider text-foreground/50">How it works</div>
<div className="space-y-2 text-foreground/70">
<div className="flex items-start gap-2">
<span className="mt-0.5 shrink-0 text-neon-cyan">1.</span>
<span>You send crypto to our receiving address.</span>
</div>
<div className="flex items-start gap-2">
<span className="mt-0.5 shrink-0 text-neon-cyan">2.</span>
<span>We verify on-chain and credit your account at market USD rate.</span>
</div>
<div className="flex items-start gap-2">
<span className="mt-0.5 shrink-0 text-neon-cyan">3.</span>
<span>You shop with USD credits. We hold the crypto and pay vendors on your behalf.</span>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,220 +1,168 @@
"use client";
import Link from "next/link";
import { useState } from "react";
import { useState, useEffect } from "react";
import { useAccount } from "@/contexts/AccountContext";
import {
loadForum,
addThread,
displayScore,
setVoteForThread,
getVoteForThread,
type ForumThread,
} from "@/lib/forumState";
const ForumBoard = () => {
const [posts, setPosts] = useState([
{
id: 1,
user: "GhostInTheShell",
avatar: "👻",
timestamp: "2 hours ago",
content: "Has anyone tested the new neural stimulant? Need reports on sideeffects.",
upvotes: 42,
replies: 12,
category: "BioEnhancements",
},
{
id: 2,
user: "ZeroCool",
avatar: "🕵️",
timestamp: "5 hours ago",
content: "Quantum counterfeit notes batch #8 passes all validation tests. Available for bulk orders.",
upvotes: 89,
replies: 24,
category: "Financial",
},
{
id: 3,
user: "CypherPunk",
avatar: "🔐",
timestamp: "1 day ago",
content: "New darknet router vulnerability discovered. Patch your firmware immediately.",
upvotes: 156,
replies: 47,
category: "Security",
},
{
id: 4,
user: "Ethereal",
avatar: "🌌",
timestamp: "2 days ago",
content: "Looking for reliable identity pack vendor with EU passports. DM with reputation score.",
upvotes: 33,
replies: 8,
category: "Identity",
},
]);
const CATEGORIES = ["All", "Security", "Market", "OPSEC", "Tech", "General"];
export default function ForumBoard() {
const { user } = useAccount();
const [threads, setThreads] = useState<ForumThread[]>([]);
const [newPost, setNewPost] = useState("");
const [selectedCategory, setSelectedCategory] = useState("All");
const [selectedCat, setSelectedCat] = useState("All");
const [voteBump, setVoteBump] = useState(0);
const [posting, setPosting] = useState(false);
const categories = ["All", "BioEnhancements", "Financial", "Security", "Identity", "Weapons", "Chemicals"];
useEffect(() => {
setThreads(loadForum().slice(0, 8));
}, []);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!newPost.trim()) return;
const newPostObj = {
id: posts.length + 1,
user: "Anonymous",
avatar: "🕶️",
timestamp: "Just now",
content: newPost,
upvotes: 0,
replies: 0,
category: "Uncategorized",
};
setPosts([newPostObj, ...posts]);
if (!newPost.trim() || posting) return;
setPosting(true);
const thread = addThread({
title: newPost.slice(0, 80),
body: newPost,
author: user?.username ?? "anon",
topicSlug: "general",
category: "General",
replies: [],
});
setThreads([thread, ...threads].slice(0, 8));
setNewPost("");
setPosting(false);
};
const filteredPosts = selectedCategory === "All"
? posts
: posts.filter(p => p.category === selectedCategory);
const handleVote = (id: string, delta: 1 | -1) => {
const existing = getVoteForThread(id);
const next = existing === delta ? 0 : delta;
setVoteForThread(id, next as 1 | -1 | 0);
setVoteBump((v) => v + 1);
};
const visible = selectedCat === "All"
? threads
: threads.filter((t) => t.category === selectedCat || t.topicSlug === selectedCat.toLowerCase());
return (
<div className="glass rounded-3xl border border-white/10 p-8">
<div className="mb-10">
<h2 className="font-orbitron text-4xl font-bold">DARKNET FORUM</h2>
<p className="mt-2 text-foreground/70">Encrypted, anonymous discussions. No logs, no traces.</p>
<div className="mb-8 flex flex-wrap items-start justify-between gap-4">
<div>
<h2 className="font-orbitron text-3xl font-bold">VOID AGGREGATE</h2>
<p className="mt-1 text-sm text-foreground/60">
{threads.length} threads shown · full board at{" "}
<Link href="/forum" className="text-neon-cyan hover:underline">/forum</Link>
</p>
</div>
</div>
{/* Category filter */}
<div className="mb-8 flex flex-wrap gap-3">
{categories.map((cat) => (
<div className="mb-6 flex flex-wrap gap-2">
{CATEGORIES.map((cat) => (
<button
key={cat}
className={`rounded-full px-4 py-2 text-sm font-medium transition-all ${selectedCategory === cat
type="button"
onClick={() => setSelectedCat(cat)}
className={`rounded-full px-4 py-1.5 text-xs font-medium transition-all ${
selectedCat === cat
? "bg-gradient-to-r from-neon-cyan to-neon-purple text-background"
: "glass border border-white/10"
: "glass border border-white/10 hover:border-neon-cyan/30"
}`}
onClick={() => setSelectedCategory(cat)}
>
{cat}
</button>
))}
</div>
{/* New post form */}
<form onSubmit={handleSubmit} className="mb-10">
<div className="glass rounded-2xl border border-white/10 p-6">
{/* Quick post form */}
<form onSubmit={handleSubmit} className="mb-8">
<div className="glass rounded-2xl border border-white/10 p-5">
<textarea
className="w-full bg-transparent text-foreground placeholder-foreground/50 focus:outline-none"
rows={3}
placeholder="Type your encrypted message... (all posts are ephemeral)"
className="w-full bg-transparent text-sm text-foreground placeholder-foreground/40 focus:outline-none"
rows={2}
placeholder={user ? `Post as @${user.username}` : "Quick post (stored in your browser)…"}
value={newPost}
onChange={(e) => setNewPost(e.target.value)}
maxLength={500}
/>
<div className="mt-4 flex items-center justify-between">
<div className="flex gap-4">
<button
type="button"
className="flex items-center gap-2 rounded-full bg-white/5 px-4 py-2 text-sm"
>
<span>🔒</span> Encrypt
</button>
<button
type="button"
className="flex items-center gap-2 rounded-full bg-white/5 px-4 py-2 text-sm"
>
<span>🕵</span> Post Anonymously
</button>
</div>
<div className="mt-3 flex items-center justify-between">
<span className="text-[10px] text-foreground/30">{newPost.length}/500 · local storage</span>
<button
type="submit"
className="rounded-full bg-gradient-to-r from-neon-cyan to-neon-purple px-6 py-3 font-bold text-background"
disabled={!newPost.trim() || posting}
className="rounded-full bg-gradient-to-r from-neon-cyan to-neon-purple px-5 py-2 text-sm font-bold text-background disabled:opacity-40"
>
PUBLISH
Post
</button>
</div>
</div>
</form>
{/* Posts list */}
<div className="space-y-6">
{filteredPosts.map((post) => (
<div key={post.id} className="glass rounded-2xl border border-white/10 p-6">
<div className="flex items-start justify-between">
<div className="flex items-center gap-4">
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-gradient-to-br from-neon-cyan to-neon-purple text-2xl">
{post.avatar}
{/* Thread list */}
<div className="space-y-4">
{visible.length === 0 ? (
<p className="py-6 text-center text-sm text-foreground/40">No threads in this category yet.</p>
) : (
visible.map((t) => {
const score = displayScore(t);
const voted = typeof window !== "undefined" ? getVoteForThread(t.id) : 0;
return (
<div key={t.id + voteBump} className="glass rounded-2xl border border-white/10 p-5">
<div className="flex items-start justify-between gap-3">
<div className="flex items-center gap-3">
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-neon-cyan/20 to-neon-purple/20 text-sm font-bold">
{(t.author ?? "?").charAt(0).toUpperCase()}
</div>
<div>
<div className="flex items-center gap-3">
<span className="font-bold">{post.user}</span>
<span className="rounded-full bg-white/5 px-3 py-1 text-xs">{post.category}</span>
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm font-bold">@{t.author ?? "anon"}</span>
{t.category && (
<span className="rounded-full bg-white/5 px-2 py-0.5 text-[10px]">{t.category}</span>
)}
</div>
<div className="text-sm text-foreground/60">{post.timestamp}</div>
<div className="text-xs text-foreground/40">{new Date(t.ts).toLocaleDateString()}</div>
</div>
</div>
<div className="flex items-center gap-4">
<button className="flex items-center gap-2 text-sm text-foreground/60 hover:text-neon-cyan">
<span></span> {post.upvotes}
</button>
<button className="flex items-center gap-2 text-sm text-foreground/60 hover:text-neon-cyan">
<span>💬</span> {post.replies}
</button>
<button className="rounded-full bg-white/5 p-2 hover:bg-white/10">
<span>🔗</span>
<div className="flex shrink-0 items-center gap-3">
<button
type="button"
onClick={() => handleVote(t.id, 1)}
className={`text-xs transition-colors ${voted === 1 ? "text-neon-cyan" : "text-foreground/40 hover:text-neon-cyan"}`}
>
{score}
</button>
<span className="text-xs text-foreground/30">💬 {t.replies.length}</span>
</div>
</div>
<div className="mt-6">
<p>{post.content}</p>
</div>
<div className="mt-6 flex items-center gap-6 border-t border-white/10 pt-6">
<button className="flex items-center gap-2 text-sm">
<span></span> Upvote
</button>
<button className="flex items-center gap-2 text-sm">
<span></span> Downvote
</button>
<button className="flex items-center gap-2 text-sm">
<span>💬</span> Reply
</button>
<button className="flex items-center gap-2 text-sm">
<span>🔐</span> Encrypt Reply
</button>
<button className="flex items-center gap-2 text-sm">
<span>🚀</span> Boost
</button>
</div>
</div>
))}
</div>
{/* Forum stats */}
<div className="mt-10 grid grid-cols-2 gap-6 md:grid-cols-4">
<div className="glass rounded-2xl border border-white/10 p-6 text-center">
<div className="text-3xl font-bold text-neon-cyan">2.4k</div>
<div className="text-sm text-foreground/60">Active Users</div>
</div>
<div className="glass rounded-2xl border border-white/10 p-6 text-center">
<div className="text-3xl font-bold text-neon-purple">18.5k</div>
<div className="text-sm text-foreground/60">Total Posts</div>
</div>
<div className="glass rounded-2xl border border-white/10 p-6 text-center">
<div className="text-3xl font-bold text-neon-pink">94%</div>
<div className="text-sm text-foreground/60">Encrypted</div>
</div>
<div className="glass rounded-2xl border border-white/10 p-6 text-center">
<div className="text-3xl font-bold text-neon-green">0</div>
<div className="text-sm text-foreground/60">Data Leaks</div>
</div>
</div>
<div className="mt-8 text-center text-xs text-foreground/40">
<p>
Preview board for threads that persist in your browser, open the{" "}
<Link href="/forum" className="text-neon-cyan hover:underline">
full forum
</Link>{" "}
(dedicated .onion maps here too).
</p>
<div className="mt-3">
<Link href="/forum" className="text-sm hover:text-neon-cyan/80">
{t.title ?? t.body?.slice(0, 100)}
</Link>
</div>
</div>
);
};
})
)}
</div>
export default ForumBoard;
<div className="mt-6 text-center">
<Link
href="/forum"
className="inline-flex items-center gap-2 rounded-full border border-neon-cyan/30 px-6 py-2 text-sm font-bold text-neon-cyan hover:bg-neon-cyan/10"
>
Open full forum
</Link>
</div>
</div>
);
}

View File

@@ -1,20 +0,0 @@
"use client";
/**
* Single required disclosure for the build: appears after every routes main content.
*/
export default function GlobalFakeSiteFooter() {
return (
<footer
className="relative z-[1] border-t-4 border-red-600 bg-[#050505] px-4 py-12 shadow-[0_-12px_40px_rgba(0,0,0,0.85)]"
role="contentinfo"
aria-label="Fiction disclosure"
>
<div className="mx-auto max-w-5xl pb-16 sm:pb-20 md:pb-24">
<p className="text-center text-4xl font-black leading-tight tracking-tight text-red-500 sm:text-5xl md:text-6xl lg:text-7xl">
This site is fake
</p>
</div>
</footer>
);
}

View File

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

View File

@@ -1,125 +1,163 @@
"use client";
import { useState } from "react";
import { useState, useRef, useEffect } from "react";
import Link from "next/link";
import { useWallet } from "@/contexts/WalletContext";
import { useAccount } from "@/contexts/AccountContext";
import { useCart } from "@/contexts/CartContext";
import { SITE_NAV_GROUPS } from "@/lib/siteNav";
const Navbar = () => {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const { isConnected, address, connect, disconnect, luxCredits, usdStoreCredit } = useWallet();
const [pagesOpen, setPagesOpen] = useState(false);
const pagesDropdownRef = useRef<HTMLDivElement>(null);
const { luxCredits, usdStoreCredit } = useWallet();
const { user, hydrated } = useAccount();
const { itemCount, hydrated: cartReady } = useCart();
const navItems = [
{ label: "Sanctuary", href: "/sanctuary", icon: "🛡️" },
{ label: "Market", href: "/market", icon: "📈" },
{ label: "Vendors", href: "/vendors", icon: "🏪" },
{ label: "Checkout", href: "/checkout", icon: "₿" },
{ label: "Forum", href: "/forum", icon: "◆" },
{ label: "Exchange", href: "/exchange", icon: "📰" },
{ label: "Barter", href: "/barter", icon: "♻️" },
{ label: "Vault", href: "/vault", icon: "🗝️" },
{ label: "Wiki", href: "/hidden-wiki", icon: "📚" },
{ label: "Atlas", href: "/darknet-atlas", icon: "🗺️" },
{ label: "Syndicate", href: "/syndicate", icon: "🕸️" },
{ label: "Void crawl", href: "/search", icon: "🔦" },
{ label: "Mirrors", href: "/account/hidden-services", icon: "🧅" },
];
const handleWalletClick = () => {
if (isConnected) {
disconnect();
} else {
connect();
}
useEffect(() => {
const onDoc = (e: MouseEvent) => {
if (!pagesDropdownRef.current?.contains(e.target as Node)) setPagesOpen(false);
};
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") setPagesOpen(false);
};
document.addEventListener("mousedown", onDoc);
document.addEventListener("keydown", onKey);
return () => {
document.removeEventListener("mousedown", onDoc);
document.removeEventListener("keydown", onKey);
};
}, []);
return (
<nav className="glass fixed top-0 left-0 right-0 z-50 mx-auto mt-4 max-w-7xl rounded-2xl border border-white/10 px-6 py-4 backdrop-blur-xl">
<div className="flex items-center justify-between">
{/* Logo */}
<div className="flex items-center gap-3">
<div className="relative">
<div className="h-10 w-10 rounded-full bg-gradient-to-br from-neon-cyan to-neon-purple p-0.5">
<div className="h-full w-full rounded-full bg-background flex items-center justify-center">
<span className="text-xl font-bold"></span>
<nav className="glass fixed top-0 left-0 right-0 z-50 mx-auto mt-2 max-w-7xl rounded-xl border border-white/10 px-4 py-2 backdrop-blur-xl md:px-5">
<div className="flex items-center justify-between gap-3">
<div className="flex min-w-0 flex-1 items-center gap-2 md:gap-3">
<Link href="/" className="flex shrink-0 items-center gap-2">
<div className="relative h-8 w-8 shrink-0 rounded-full bg-gradient-to-br from-neon-cyan to-neon-purple p-0.5">
<div className="flex h-full w-full items-center justify-center rounded-full bg-background">
<span className="text-sm font-bold"></span>
</div>
</div>
<div className="absolute -inset-1 -z-10 animate-pulse rounded-full bg-neon-cyan blur-md opacity-30"></div>
</div>
<Link href="/" className="font-orbitron text-2xl font-bold tracking-tighter">
Cyber<span className="text-neon-cyan">Lux</span>
</Link>
<span className="hidden rounded-full bg-muted px-3 py-1 text-xs font-medium text-neon-green sm:inline">
v1.0 ALPHA
<span className="font-orbitron truncate text-base font-bold tracking-tight md:text-lg">
Eldritch<span className="text-neon-cyan">Weave</span>
</span>
</div>
{/* Desktop Navigation */}
<div className="hidden max-w-lg flex-wrap items-center justify-end gap-x-3 gap-y-1 md:flex lg:max-w-2xl lg:gap-x-4 xl:max-w-none xl:flex-nowrap xl:gap-x-5">
{navItems.map((item) => (
<Link
key={item.label}
href={item.href}
className="group relative text-sm font-medium text-foreground/80 transition-colors hover:text-neon-cyan"
>
<span className="mr-1 opacity-60">{item.icon}</span>
{item.label}
<span className="absolute -bottom-1 left-0 h-0.5 w-0 bg-gradient-to-r from-neon-cyan to-neon-purple transition-all group-hover:w-full"></span>
</Link>
<span className="hidden rounded-full bg-muted/80 px-2 py-0.5 text-[10px] font-medium text-neon-green/90 sm:inline">
v1 · spellcraft
</span>
<div ref={pagesDropdownRef} className="relative ml-1 hidden md:block">
<button
type="button"
aria-expanded={pagesOpen}
aria-haspopup="true"
onClick={() => setPagesOpen((o) => !o)}
className="flex items-center gap-1 rounded-lg border border-white/15 bg-white/5 px-3 py-1.5 text-xs font-semibold text-foreground/85 transition hover:border-neon-cyan/35 hover:text-neon-cyan"
>
All pages
<span className="text-[10px] opacity-70">{pagesOpen ? "▴" : "▾"}</span>
</button>
{pagesOpen ? (
<div className="absolute left-0 top-full z-[60] mt-1 max-h-[min(70vh,520px)] w-[min(100vw-2rem,380px)] overflow-y-auto rounded-xl border border-white/15 bg-[#0a0a0f]/95 p-3 shadow-2xl shadow-black/50 backdrop-blur-xl">
{SITE_NAV_GROUPS.map((group) => (
<div key={group.title} className="mb-3 last:mb-0">
<p className="mb-1.5 px-1 font-mono text-[9px] font-bold uppercase tracking-[0.2em] text-foreground/40">
{group.title}
</p>
<ul className="space-y-0.5">
{group.items.map((item) => (
<li key={item.href}>
<Link
href={item.href}
className="flex items-center gap-2 rounded-lg px-2 py-1.5 text-xs text-foreground/80 transition hover:bg-white/10 hover:text-neon-cyan"
onClick={() => setPagesOpen(false)}
>
<span className="w-5 text-center opacity-70">{item.icon}</span>
<span className="truncate">{item.label}</span>
</Link>
</li>
))}
</ul>
</div>
))}
</div>
) : null}
</div>
{/* Actions */}
<div className="flex items-center gap-3 md:gap-4">
<Link
href="/market"
className="hidden text-xs font-medium text-foreground/70 transition hover:text-neon-cyan lg:inline"
>
Market
</Link>
<Link
href="/checkout"
className="hidden text-xs font-medium text-foreground/70 transition hover:text-neon-purple lg:inline"
>
Checkout
</Link>
</div>
<div className="flex shrink-0 items-center gap-2 md:gap-3">
{cartReady ? (
<Link
href="/checkout"
className="relative hidden rounded-full border border-white/10 px-3 py-2 text-sm text-foreground/80 hover:border-neon-cyan/40 hover:text-neon-cyan sm:inline-flex"
className="relative inline-flex rounded-full border border-white/10 px-2.5 py-1.5 text-xs text-foreground/80 hover:border-neon-cyan/40 hover:text-neon-cyan"
title="Cart"
>
🛒
{itemCount > 0 ? (
<span className="absolute -right-1 -top-1 flex h-5 min-w-5 items-center justify-center rounded-full bg-neon-pink px-1 text-[10px] font-bold text-background">
<span className="absolute -right-1 -top-1 flex h-4 min-w-4 items-center justify-center rounded-full bg-neon-pink px-0.5 text-[9px] font-bold text-background">
{itemCount > 99 ? "99+" : itemCount}
</span>
) : null}
</Link>
) : null}
<div className="hidden md:flex items-center gap-3">
<div className="flex items-center gap-2 rounded-full bg-white/5 px-4 py-2 border border-white/10">
<span className="text-xs text-foreground/60">LUX</span>
<span className="font-orbitron text-sm text-neon-green">{luxCredits.toLocaleString()}</span>
<div className="hidden items-center gap-1.5 sm:flex">
<div
className="flex items-center gap-1 rounded-full border border-white/10 bg-white/5 px-2 py-1 text-[10px]"
title="LUX credits"
>
<span className="text-foreground/50">LUX</span>
<span className="font-orbitron text-[11px] text-neon-green">{luxCredits.toLocaleString()}</span>
</div>
<div
className="flex items-center gap-2 rounded-full bg-white/5 px-4 py-2 border border-neon-cyan/20"
title="Verified Bitcoin deposit balance (USD)"
className="flex items-center gap-1 rounded-full border border-neon-cyan/20 bg-white/5 px-2 py-1 text-[10px]"
title="Bitcoin-funded USD balance"
>
<span className="text-xs text-foreground/60">USD</span>
<span className="font-orbitron text-sm text-neon-cyan">${usdStoreCredit.toFixed(2)}</span>
<span className="text-foreground/50">$</span>
<span className="font-orbitron text-[11px] text-neon-cyan">{usdStoreCredit.toFixed(2)}</span>
</div>
</div>
{hydrated ? (
user ? (
<>
<Link
href="/account/add-funds"
className="hidden rounded-full border border-neon-green/30 bg-neon-green/10 px-2.5 py-1 text-[10px] font-bold uppercase tracking-wide text-neon-green hover:bg-neon-green/15 sm:inline-flex"
>
+Funds
</Link>
<Link
href="/dashboard"
className="hidden items-center gap-2 rounded-full border border-neon-cyan/30 bg-neon-cyan/10 px-4 py-2 text-sm font-medium text-neon-cyan hover:bg-neon-cyan/20 sm:flex"
className="hidden max-w-[6rem] truncate rounded-full border border-neon-cyan/25 bg-neon-cyan/10 px-2.5 py-1 text-[11px] font-medium text-neon-cyan hover:bg-neon-cyan/18 sm:inline"
>
<span className="max-w-[8rem] truncate">{user.displayName}</span>
{user.displayName}
</Link>
</>
) : (
<>
<Link
href="/sign-in"
className="hidden rounded-full border border-white/15 px-4 py-2 text-sm font-medium text-foreground/80 hover:border-neon-cyan/50 hover:text-neon-cyan sm:inline-flex"
className="hidden rounded-full border border-white/12 px-2.5 py-1 text-[11px] font-medium text-foreground/80 hover:border-neon-cyan/45 hover:text-neon-cyan sm:inline-flex"
>
Sign in
</Link>
<Link
href="/sign-up"
className="hidden rounded-full border border-neon-purple/35 bg-neon-purple/10 px-4 py-2 text-sm font-medium text-neon-purple hover:bg-neon-purple/20 md:inline-flex"
className="hidden rounded-full border border-neon-purple/30 bg-neon-purple/10 px-2.5 py-1 text-[11px] font-medium text-neon-purple hover:bg-neon-purple/18 md:inline-flex"
>
Register
</Link>
@@ -127,51 +165,39 @@ const Navbar = () => {
)
) : null}
<button
onClick={handleWalletClick}
className={`flex items-center gap-2 rounded-full px-4 py-2 font-medium transition-all ${isConnected
? "bg-gradient-to-r from-neon-green to-neon-cyan text-background"
: "glow-border text-neon-cyan hover:bg-white/5"
}`}
>
<span className="text-lg">{isConnected ? "🟢" : "⚡"}</span>
{isConnected ? `${address?.slice(0, 6)}...${address?.slice(-4)}` : "Connect Wallet"}
</button>
<button
className="rounded-full bg-white/5 p-2 hover:bg-white/10"
className="rounded-full bg-white/5 p-1.5 hover:bg-white/10 md:hidden"
aria-label="Toggle menu"
onClick={() => setIsMenuOpen(!isMenuOpen)}
>
<div className="h-5 w-5 space-y-1.5">
<div className={`h-0.5 bg-neon-cyan transition-transform ${isMenuOpen ? "translate-y-2 rotate-45" : ""}`}></div>
<div className={`h-0.5 bg-neon-purple transition-opacity ${isMenuOpen ? "opacity-0" : ""}`}></div>
<div className={`h-0.5 bg-neon-pink transition-transform ${isMenuOpen ? "-translate-y-2 -rotate-45" : ""}`}></div>
<div className="h-4 w-4 space-y-1">
<div className={`h-0.5 bg-neon-cyan transition-transform ${isMenuOpen ? "translate-y-1.5 rotate-45" : ""}`} />
<div className={`h-0.5 bg-neon-purple transition-opacity ${isMenuOpen ? "opacity-0" : ""}`} />
<div className={`h-0.5 bg-neon-pink transition-transform ${isMenuOpen ? "-translate-y-1.5 -rotate-45" : ""}`} />
</div>
</button>
</div>
</div>
{/* Mobile Menu */}
{/* Mobile: full menu + page list */}
{isMenuOpen && (
<div className="glass mt-4 rounded-xl border border-white/10 p-4 backdrop-blur-xl md:hidden">
<div className="mb-3 flex gap-2 border-b border-white/10 pb-3">
<div className="glass mt-2 max-h-[75vh] overflow-y-auto rounded-lg border border-white/10 p-3 backdrop-blur-xl md:hidden">
<div className="mb-2 flex flex-wrap gap-2 border-b border-white/10 pb-2">
{cartReady ? (
<Link
href="/checkout"
className="relative flex-1 rounded-lg border border-white/15 py-2 text-center text-sm font-medium"
className="relative flex-1 rounded-lg border border-white/15 py-2 text-center text-xs font-medium"
onClick={() => setIsMenuOpen(false)}
>
🛒 Cart
{itemCount > 0 ? (
<span className="ml-1 rounded bg-neon-pink px-1.5 text-[10px] font-bold text-background">
{itemCount}
</span>
<span className="ml-1 rounded bg-neon-pink px-1 text-[9px] font-bold text-background">{itemCount}</span>
) : null}
</Link>
) : null}
{hydrated && user ? (
<Link
href="/dashboard"
className="flex-1 rounded-lg bg-neon-cyan/15 py-2 text-center text-sm font-bold text-neon-cyan"
className="flex-1 rounded-lg bg-neon-cyan/15 py-2 text-center text-xs font-bold text-neon-cyan"
onClick={() => setIsMenuOpen(false)}
>
Dashboard
@@ -179,51 +205,31 @@ const Navbar = () => {
) : hydrated ? (
<Link
href="/sign-in"
className="flex-1 rounded-lg border border-white/15 py-2 text-center text-sm font-medium"
className="flex-1 rounded-lg border border-white/15 py-2 text-center text-xs font-medium"
onClick={() => setIsMenuOpen(false)}
>
Sign in
</Link>
) : null}
{hydrated && !user ? (
<Link
href="/sign-up"
className="flex-1 rounded-lg border border-neon-purple/40 py-2 text-center text-sm text-neon-purple"
onClick={() => setIsMenuOpen(false)}
>
Register
</Link>
) : null}
</div>
<div className="grid grid-cols-2 gap-3">
{navItems.map((item) => (
{SITE_NAV_GROUPS.map((group) => (
<div key={group.title} className="mb-3">
<p className="mb-1 font-mono text-[9px] font-bold uppercase tracking-widest text-foreground/35">{group.title}</p>
<div className="grid grid-cols-2 gap-1">
{group.items.map((item) => (
<Link
key={item.label}
key={item.href}
href={item.href}
className="flex items-center gap-2 rounded-lg bg-white/5 p-3 text-sm font-medium transition-colors hover:bg-white/10"
className="flex items-center gap-1.5 rounded-md bg-white/5 p-2 text-[11px] font-medium"
onClick={() => setIsMenuOpen(false)}
>
<span className="text-lg">{item.icon}</span>
{item.label}
<span>{item.icon}</span>
<span className="truncate">{item.label}</span>
</Link>
))}
</div>
<div className="mt-4 flex gap-3 border-t border-white/10 pt-4 text-xs">
<Link
href="/sanctuary"
className="flex-1 rounded-lg border border-white/10 px-3 py-2 text-center text-foreground/70 hover:border-neon-cyan/40 hover:text-neon-cyan"
onClick={() => setIsMenuOpen(false)}
>
Sanctuary
</Link>
<Link
href="/account/hidden-services"
className="flex-1 rounded-lg border border-white/10 px-3 py-2 text-center text-foreground/70 hover:border-neon-purple/40 hover:text-neon-purple"
onClick={() => setIsMenuOpen(false)}
>
Onion map
</Link>
</div>
))}
</div>
)}
</nav>

View File

@@ -0,0 +1,111 @@
"use client";
import { useState } from "react";
import Link from "next/link";
import { useAccount } from "@/contexts/AccountContext";
import { useWallet } from "@/contexts/WalletContext";
type Props = {
/** USD amount to charge (2 decimals) */
amountUsd: number;
/** Receipt / vault title */
title: string;
description?: string;
className?: string;
size?: "sm" | "md";
/** Override LUX granted (default: based on USD amount) */
luxBonus?: number;
/** Called after a successful charge */
onSuccess?: () => void;
};
/**
* Spend verified Bitcoin-funded USD balance without going through full checkout cart.
*/
export default function PayWithUsdBalanceButton({
amountUsd,
title,
description,
className = "",
size = "md",
luxBonus,
onSuccess,
}: Props) {
const { user } = useAccount();
const { usdStoreCredit, spendUsdStoreCredit, earnLuxCredits, addVaultReceipt } = useWallet();
const [msg, setMsg] = useState<{ kind: "ok" | "err"; text: string } | null>(null);
const [busy, setBusy] = useState(false);
const safe = Math.round(amountUsd * 100) / 100;
const pad = size === "sm" ? "px-3 py-1.5 text-xs" : "px-5 py-2.5 text-sm";
const pay = () => {
setMsg(null);
if (!user) {
setMsg({ kind: "err", text: "Sign in to spend your USD balance." });
return;
}
if (!(safe > 0)) {
setMsg({ kind: "err", text: "Invalid amount." });
return;
}
setBusy(true);
try {
if (!spendUsdStoreCredit(safe)) {
setMsg({
kind: "err",
text: `Need $${safe.toFixed(2)} — you have $${usdStoreCredit.toFixed(2)}. Add funds first.`,
});
return;
}
const lux =
luxBonus != null && Number.isFinite(luxBonus)
? Math.max(0, Math.floor(luxBonus))
: Math.min(500, Math.max(10, Math.floor(safe)));
earnLuxCredits(lux);
const oid =
typeof crypto !== "undefined" && "randomUUID" in crypto
? `PAY-${crypto.randomUUID().slice(0, 8).toUpperCase()}`
: `PAY-${Date.now().toString(36).toUpperCase()}`;
addVaultReceipt({
id: oid,
title,
desc: description ?? `${title} · $${safe.toFixed(2)} USD`,
date: new Date().toISOString().slice(0, 10),
severity: "normal",
});
setMsg({ kind: "ok", text: `Paid $${safe.toFixed(2)}. +${lux} LUX · receipt ${oid}` });
onSuccess?.();
} finally {
setBusy(false);
}
};
return (
<div className="space-y-2">
<button
type="button"
disabled={busy || safe <= 0}
onClick={pay}
className={`rounded-full border border-neon-cyan/40 bg-neon-cyan/15 font-bold text-neon-cyan transition hover:bg-neon-cyan/25 disabled:opacity-40 ${pad} ${className}`}
>
{busy ? "Processing…" : `Pay $${safe.toFixed(2)} with balance`}
</button>
{!user ? (
<p className="text-xs text-foreground/55">
<Link href="/sign-in" className="text-neon-cyan underline">
Sign in
</Link>{" "}
to use BTC-funded USD credit.
</p>
) : (
<p className="text-xs text-foreground/45">
Balance: <span className="font-orbitron text-neon-green">${usdStoreCredit.toFixed(2)}</span>
</p>
)}
{msg ? (
<p className={`text-xs ${msg.kind === "ok" ? "text-neon-green" : "text-red-400"}`}>{msg.text}</p>
) : null}
</div>
);
}

View File

@@ -0,0 +1,156 @@
"use client";
import { useEffect, useState, useCallback } from "react";
import Link from "next/link";
import { useAccount } from "@/contexts/AccountContext";
const EPOCH = new Date("2026-04-14T00:00:00Z").getTime();
const INTERVAL = 7 * 24 * 60 * 60 * 1000;
function getNextDraw() {
const now = Date.now();
return EPOCH + (Math.floor((now - EPOCH) / INTERVAL) + 1) * INTERVAL;
}
function fmt(ms: number) {
if (ms <= 0) return "DRAWING NOW";
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")}`;
}
type RaffleStats = { totalTickets: number; participants: number; drawAt: number };
export default function RaffleCountdownBanner() {
const { user } = useAccount();
const [remaining, setRemaining] = useState(() => getNextDraw() - Date.now());
const [stats, setStats] = useState<RaffleStats | null>(null);
const [buying, setBuying] = useState(false);
const [qty, setQty] = useState(1);
const [msg, setMsg] = useState<{ text: string; ok: boolean } | null>(null);
const [open, setOpen] = useState(false);
const [dismissed, setDismissed] = useState(false);
useEffect(() => {
const t = setInterval(() => setRemaining(getNextDraw() - Date.now()), 1000);
return () => clearInterval(t);
}, []);
const fetchStats = useCallback(async () => {
try {
const r = await fetch("/api/raffle/buy");
const d = (await r.json()) as RaffleStats & { ok: boolean };
if (d.ok) setStats(d);
} catch { /* ignore */ }
}, []);
useEffect(() => { void fetchStats(); }, [fetchStats]);
const buyTickets = async () => {
if (!user) return;
setBuying(true);
setMsg(null);
try {
const r = await fetch("/api/raffle/buy", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ handle: user.username, quantity: qty }),
});
const d = (await r.json()) as { ok: boolean; error?: string; ticketsBought?: number; voidBalance?: number };
if (d.ok) {
setMsg({ text: `${d.ticketsBought} ticket${(d.ticketsBought ?? 1) > 1 ? "s" : ""} entered! Balance: ${d.voidBalance} VOID`, ok: true });
void fetchStats();
} else {
setMsg({ text: d.error ?? "Failed", ok: false });
}
} catch {
setMsg({ text: "Network error", ok: false });
} finally {
setBuying(false);
}
};
if (dismissed) return null;
return (
<div className="sticky top-0 z-50 w-full bg-gradient-to-r from-[#0d0d0d] via-[#111827] to-[#0d0d0d] border-b border-[#00ff66]/20 shadow-[0_2px_20px_rgba(0,255,102,0.08)]">
<div className="mx-auto flex max-w-7xl items-center justify-between gap-2 px-4 py-1.5 text-xs">
{/* Left: timer + pot */}
<div className="flex items-center gap-3 min-w-0">
<span className="text-[#00ff66]/50 font-mono hidden sm:inline"> WEEKLY VOID RAFFLE</span>
<div className="font-mono text-[#00ff66] text-sm font-bold tabular-nums tracking-widest">
{fmt(remaining)}
</div>
{stats && (
<span className="text-zinc-500 hidden md:inline">
{stats.totalTickets} tickets · {stats.participants} players
</span>
)}
</div>
{/* Centre: quick buy */}
<div className="flex items-center gap-2">
{user ? (
open ? (
<div className="flex items-center gap-2 animate-in fade-in slide-in-from-top-1">
<button
onClick={() => setQty((q) => Math.max(1, q - 1))}
className="w-6 h-6 rounded border border-[#00ff66]/30 text-[#00ff66] hover:bg-[#00ff66]/10 font-bold leading-none"
></button>
<span className="font-mono text-[#00ff66] w-6 text-center">{qty}</span>
<button
onClick={() => setQty((q) => Math.min(100, q + 1))}
className="w-6 h-6 rounded border border-[#00ff66]/30 text-[#00ff66] hover:bg-[#00ff66]/10 font-bold leading-none"
>+</button>
<button
onClick={buyTickets}
disabled={buying}
className="rounded bg-[#00ff66]/20 border border-[#00ff66]/40 px-3 py-1 font-bold text-[#00ff66] hover:bg-[#00ff66]/30 disabled:opacity-50 transition-colors text-xs"
>
{buying ? "…" : `BUY ${qty} · ${qty} VOID`}
</button>
{msg && (
<span className={msg.ok ? "text-[#00ff66]" : "text-red-400"}>
{msg.text}
</span>
)}
</div>
) : (
<button
onClick={() => setOpen(true)}
className="rounded border border-[#00ff66]/30 bg-[#00ff66]/10 px-3 py-1 font-bold text-[#00ff66] hover:bg-[#00ff66]/20 transition-colors"
>
🎟 BUY TICKET · 1 VOID
</button>
)
) : (
<Link
href="/raffle"
className="rounded border border-[#00ff66]/30 bg-[#00ff66]/10 px-3 py-1 font-bold text-[#00ff66] hover:bg-[#00ff66]/20 transition-colors"
>
🎟 WEEKLY RAFFLE
</Link>
)}
</div>
{/* Right: prize + close */}
<div className="flex items-center gap-3">
<Link href="/raffle" className="text-zinc-500 hover:text-[#00ff66] hidden sm:inline transition-colors">
500 VOID top prize
</Link>
<button
onClick={() => { setOpen(false); setDismissed(true); }}
className="text-zinc-600 hover:text-zinc-400 text-base leading-none transition-colors"
aria-label="Close raffle banner"
>
</button>
</div>
</div>
</div>
);
}

View File

@@ -23,7 +23,7 @@ export function WebringBadge({ storeHref = "/" }: { storeHref?: string }) {
</Link>
</div>
<div className="mt-4 text-xs text-foreground/40">
Disclaimer: parody universe. No real goods. No real money. Just immaculate confusion.
Shadow-routed microsite · deploy your own onion from the hub <Link href="/launch" className="text-neon-cyan hover:underline">/launch</Link>
</div>
</div>
);

View File

@@ -0,0 +1,46 @@
"use client";
import Link from "next/link";
import { useWallet } from "@/contexts/WalletContext";
import { useCart } from "@/contexts/CartContext";
import { useAccount } from "@/contexts/AccountContext";
export default function MarketCommerceBar() {
const { usdStoreCredit, luxCredits } = useWallet();
const { itemCount, hydrated } = useCart();
const { user, hydrated: accReady } = useAccount();
return (
<div className="flex flex-wrap items-center justify-end gap-2 border-b border-[#00ff41]/15 bg-[#040805]/90 px-3 py-2 font-mono text-[10px] text-[#8fccb0]">
<span className="text-[#5a8f6a]">Spendable</span>
<span title="Bitcoin-funded USD">
USD <strong className="text-[#7af598]">${usdStoreCredit.toFixed(2)}</strong>
</span>
<span className="text-[#00ff41]/25">|</span>
<span title="LUX credits">
LUX <strong className="text-[#b4ffcc]">{luxCredits.toLocaleString()}</strong>
</span>
{accReady && !user ? (
<>
<span className="text-[#00ff41]/25">|</span>
<Link href="/sign-in?next=/market" className="text-[#7af598] underline hover:text-white">
Sign in to pay
</Link>
</>
) : null}
<span className="text-[#00ff41]/25">|</span>
<Link
href="/checkout"
className="inline-flex items-center gap-1 rounded border border-[#00ff41]/35 px-2 py-1 text-[#c8ffd8] hover:border-[#00ff41]/60"
>
🛒 Checkout
{hydrated && itemCount > 0 ? (
<span className="rounded bg-[#ff00aa]/90 px-1 text-[9px] font-bold text-black">{itemCount}</span>
) : null}
</Link>
<Link href="/account/add-funds" className="rounded border border-[#00ff41]/20 px-2 py-1 hover:border-[#00ff41]/45">
+Funds
</Link>
</div>
);
}

View File

@@ -2,6 +2,7 @@
import Link from "next/link";
import { usePathname } from "next/navigation";
import MarketCommerceBar from "@/components/market/MarketCommerceBar";
import LexiconStrip from "@/components/site/LexiconStrip";
import { MARKET_ATMOSPHERE_TAGS } from "@/lib/cyberluxZoneLexicon";
import { SHOP_PRODUCT_COUNT } from "@/lib/shopCatalog";
@@ -70,15 +71,18 @@ export default function MarketSiteChrome({ children }: { children: React.ReactNo
/>
<header className="sticky top-0 z-30 border-b border-[#00ff41]/20 bg-[#030806]/95 backdrop-blur-md">
<div className="mx-auto flex max-w-[1600px] flex-col gap-3 px-4 py-4 lg:flex-row lg:items-center lg:justify-between">
<div className="mx-auto max-w-[1600px]">
<MarketCommerceBar />
</div>
<div className="mx-auto flex max-w-[1600px] flex-col gap-3 px-4 py-3 lg:flex-row lg:items-center lg:justify-between">
<div>
<p className="font-mono text-[9px] uppercase tracking-[0.5em] text-[#00ff41]/45">sovereign catalog · multi-layer</p>
<Link href="/market" className="mt-1 block">
<h1 className="font-mono text-2xl font-black tracking-tight text-[#7af598] md:text-3xl">
<h1 className="font-mono text-xl font-black tracking-tight text-[#7af598] md:text-2xl">
The Candy Shop
</h1>
</Link>
<p className="mt-1 max-w-xl font-mono text-[11px] text-[#00ff41]/55">
<p className="mt-1 max-w-xl font-mono text-[10px] text-[#00ff41]/55 md:text-[11px]">
<strong className="text-[#9dffc4]">{SHOP_PRODUCT_COUNT}</strong> SKUs ·{" "}
<strong className="text-[#9dffc4]">{SHOP_SELLER_COUNT}</strong> stalls not a plugin mall; one cart, one checkout, four trust layers.
</p>

View File

@@ -1,28 +1,19 @@
"use client";
import { createContext, useContext, useEffect, useMemo, useState, ReactNode } from "react";
import { createContext, useCallback, useContext, useEffect, useMemo, useState, ReactNode } from "react";
import type { VaultReceipt, VaultState } from "@/lib/metaGame";
import { addKey, addReceipt, defaultVaultState, loadVaultState, saveVaultState, setFlag } from "@/lib/metaGame";
import {
loadClaimedTxids,
loadUsdStoreCredit,
saveClaimedTxids,
saveUsdStoreCredit,
} from "@/lib/storeCreditStorage";
import { getLedgerRow, migrateLegacyDeviceLedgerIfNeeded, setLedgerRow } from "@/lib/storeCreditStorage";
import { useAccount } from "@/contexts/AccountContext";
interface WalletContextType {
isConnected: boolean;
address: string | null;
chainId: number | null;
connect: () => Promise<void>;
disconnect: () => void;
balance: string;
luxCredits: number;
usdStoreCredit: number;
luxCredits: number;
spendLuxCredits: (amount: number) => boolean;
earnLuxCredits: (amount: number) => void;
spendUsdStoreCredit: (amount: number) => boolean;
verifyBtcDeposit: (txid: string) => Promise<{ ok: boolean; error?: string; creditedUsd?: number }>;
creditBtcPayInvoice: (invoiceId: string, usdAmount: number) => { ok: boolean; error?: string };
vault: VaultState;
collectKey: (key: string) => boolean;
addVaultReceipt: (receipt: VaultReceipt) => void;
@@ -44,82 +35,84 @@ interface WalletProviderProps {
}
export const WalletProvider = ({ children }: WalletProviderProps) => {
const [isConnected, setIsConnected] = useState(false);
const [address, setAddress] = useState<string | null>(null);
const [chainId, setChainId] = useState<number | null>(null);
const [balance, setBalance] = useState("0.0");
const [luxCredits, setLuxCredits] = useState(0);
const { user } = useAccount();
const handle = user?.username ?? null;
const [usdStoreCredit, setUsdStoreCredit] = useState(0);
const [usdHydrated, setUsdHydrated] = useState(false);
const [luxCredits, setLuxCredits] = useState(0);
const [vault, setVault] = useState<VaultState>(() => defaultVaultState());
const mockAddress = "0x71C7...e4a2";
const mockChainId = 1;
const mockBalance = "2.45";
const startingLuxCredits = 5000;
const connect = async () => {
// Simulate wallet connection delay
await new Promise((resolve) => setTimeout(resolve, 800));
setIsConnected(true);
setAddress(mockAddress);
setChainId(mockChainId);
setBalance(mockBalance);
setLuxCredits(startingLuxCredits);
};
const disconnect = () => {
setIsConnected(false);
setAddress(null);
setChainId(null);
setBalance("0.0");
useEffect(() => {
if (!handle) {
setUsdStoreCredit(0);
setLuxCredits(0);
};
return;
}
migrateLegacyDeviceLedgerIfNeeded(handle);
const row = getLedgerRow(handle);
setUsdStoreCredit(row.usd);
setLuxCredits(row.lux);
}, [handle]);
useEffect(() => {
setVault(loadVaultState());
setUsdStoreCredit(loadUsdStoreCredit());
setUsdHydrated(true);
}, []);
setVault(loadVaultState(handle));
}, [handle]);
useEffect(() => {
saveVaultState(vault);
}, [vault]);
saveVaultState(handle, vault);
}, [vault, handle]);
useEffect(() => {
if (!usdHydrated) return;
saveUsdStoreCredit(usdStoreCredit);
}, [usdStoreCredit, usdHydrated]);
const spendLuxCredits = (amount: number) => {
const spendLuxCredits = useCallback((amount: number) => {
if (!handle) return false;
const safeAmount = Number.isFinite(amount) ? Math.max(0, Math.floor(amount)) : 0;
if (safeAmount <= 0) return true;
if (luxCredits < safeAmount) return false;
setLuxCredits((c) => c - safeAmount);
const row = getLedgerRow(handle);
if (row.lux < safeAmount) return false;
const nextLux = row.lux - safeAmount;
setLedgerRow(handle, { usd: row.usd, lux: nextLux, claimedTxids: row.claimedTxids });
setLuxCredits(nextLux);
return true;
};
}, [handle]);
const earnLuxCredits = (amount: number) => {
const earnLuxCredits = useCallback(
(amount: number) => {
if (!handle) return;
const safeAmount = Number.isFinite(amount) ? Math.max(0, Math.floor(amount)) : 0;
if (safeAmount <= 0) return;
setLuxCredits((c) => c + safeAmount);
};
const row = getLedgerRow(handle);
const nextLux = row.lux + safeAmount;
setLedgerRow(handle, { usd: row.usd, lux: nextLux, claimedTxids: row.claimedTxids });
setLuxCredits(nextLux);
},
[handle],
);
const spendUsdStoreCredit = (amount: number) => {
const spendUsdStoreCredit = useCallback(
(amount: number) => {
if (!handle) return false;
const safe = Number.isFinite(amount) ? Math.max(0, Math.round(amount * 100) / 100) : 0;
if (safe <= 0) return true;
if (usdStoreCredit + 1e-9 < safe) return false;
setUsdStoreCredit((u) => Math.round((u - safe) * 100) / 100);
const row = getLedgerRow(handle);
if (row.usd + 1e-9 < safe) return false;
const nextUsd = Math.round((row.usd - safe) * 100) / 100;
setLedgerRow(handle, { usd: nextUsd, lux: row.lux, claimedTxids: row.claimedTxids });
setUsdStoreCredit(nextUsd);
return true;
};
},
[handle],
);
const verifyBtcDeposit = async (txid: string) => {
const verifyBtcDeposit = useCallback(
async (txid: string) => {
if (!handle) {
return { ok: false, error: "Sign in so deposits credit your handles balance." };
}
const normalized = String(txid || "").trim();
if (!/^[a-fA-F0-9]{64}$/.test(normalized)) {
return { ok: false, error: "Enter a valid 64-character transaction id" };
}
const claimed = loadClaimedTxids();
if (claimed.includes(normalized)) {
const row = getLedgerRow(handle);
if (row.claimedTxids.includes(normalized)) {
return { ok: false, error: "This transaction was already used to add credit" };
}
try {
@@ -136,15 +129,35 @@ export const WalletProvider = ({ children }: WalletProviderProps) => {
if (!data.ok || typeof data.creditedUsd !== "number") {
return { ok: false, error: data.error || "Verification failed" };
}
claimed.push(normalized);
saveClaimedTxids(claimed);
const add = Math.round(data.creditedUsd * 100) / 100;
setUsdStoreCredit((u) => Math.round((u + add) * 100) / 100);
const nextUsd = Math.round((row.usd + add) * 100) / 100;
const claimed = [...row.claimedTxids, normalized];
setLedgerRow(handle, { usd: nextUsd, lux: row.lux, claimedTxids: claimed });
setUsdStoreCredit(nextUsd);
return { ok: true, creditedUsd: add };
} catch {
return { ok: false, error: "Network error while verifying" };
}
};
},
[handle],
);
const creditBtcPayInvoice = useCallback(
(invoiceId: string, usdAmount: number) => {
if (!handle) return { ok: false, error: "Sign in first" };
const id = String(invoiceId || "").trim();
if (!id) return { ok: false, error: "Invalid invoice ID" };
const safe = Number.isFinite(usdAmount) ? Math.max(0, Math.round(usdAmount * 100) / 100) : 0;
if (safe <= 0) return { ok: false, error: "Invalid amount" };
const row = getLedgerRow(handle);
if (row.claimedTxids.includes(id)) return { ok: false, error: "Invoice already credited" };
const nextUsd = Math.round((row.usd + safe) * 100) / 100;
setLedgerRow(handle, { usd: nextUsd, lux: row.lux, claimedTxids: [...row.claimedTxids, id] });
setUsdStoreCredit(nextUsd);
return { ok: true };
},
[handle],
);
const collectKey = (key: string) => {
const trimmed = String(key || "").trim();
@@ -166,31 +179,29 @@ export const WalletProvider = ({ children }: WalletProviderProps) => {
const value = useMemo(
() => ({
isConnected,
address,
chainId,
connect,
disconnect,
balance,
luxCredits,
usdStoreCredit,
luxCredits,
spendLuxCredits,
earnLuxCredits,
spendUsdStoreCredit,
verifyBtcDeposit,
creditBtcPayInvoice,
vault,
collectKey,
addVaultReceipt,
setVaultFlag,
}),
[isConnected, address, chainId, balance, luxCredits, usdStoreCredit, vault],
[
usdStoreCredit,
luxCredits,
spendLuxCredits,
earnLuxCredits,
spendUsdStoreCredit,
verifyBtcDeposit,
creditBtcPayInvoice,
vault,
],
);
return (
<WalletContext.Provider
value={value}
>
{children}
</WalletContext.Provider>
);
return <WalletContext.Provider value={value}>{children}</WalletContext.Provider>;
};

View File

@@ -35,17 +35,17 @@ const seeds: BarterListing[] = [
id: "barter-seed-2",
author: "ledger_moth",
title: "Monero for physical dead-tree cipher zines",
have: "XMR at spot-ish (fictional amount — negotiate in-thread).",
have: "XMR at spot-ish — negotiate in-thread.",
want: "High-res scans of 80s crypto zines, OCR optional.",
lane: "goods",
body: "Escrow via hub wallet credit only for this sim. Otherwise plaintext terms only.",
body: "Escrow via hub USD balance after Bitcoin verify, or agree plaintext terms in-thread.",
ts: Date.now() - 7200000 * 6,
},
{
id: "barter-seed-3",
author: "patchbay_7",
title: "Studio time ↔ exploit courseware slides",
have: "4h mixing desk + mastering chain on airgapped DAW session (fiction).",
have: "4h mixing desk + mastering chain on airgapped DAW session (training scenario).",
want: "De-weaponized slide deck on heap grooming for class (no live targets).",
lane: "data",
body: "You send PDF, I send stems. Both sides verify hashes before swap.",
@@ -68,7 +68,7 @@ const seeds: BarterListing[] = [
have: "Laser-cut acrylic proof of attendance tokens (larp).",
want: "Someone to crunch log anonymization on an offline CSV (class data).",
lane: "services",
body: "Drop coordination fiction only. Real world: use your institutions lab policy.",
body: "Coordinate only through your institutions lab policy — this board is data, not logistics.",
ts: Date.now() - 7200000 * 14,
},
];

149
lib/btcpay.ts Normal file
View File

@@ -0,0 +1,149 @@
/**
* BTCPay Server Greenfield API v1 client.
*
* Env vars required (set in .env.local):
* BTCPAY_URL e.g. http://10.30.20.237
* BTCPAY_API_KEY generated in BTCPay → Account → Manage account → API keys
* BTCPAY_STORE_ID found in BTCPay → Settings → General (the alphanumeric ID in the URL)
*/
export type BtcPayInvoiceStatus =
| "New"
| "Processing"
| "Expired"
| "Invalid"
| "Settled";
export type BtcPayInvoice = {
id: string;
status: BtcPayInvoiceStatus;
amount: string;
currency: string;
checkoutLink: string;
createdTime: number;
expirationTime: number;
metadata?: Record<string, string>;
};
export type BtcPayPaymentMethod = {
paymentMethod: string; // "BTC"
destination: string; // bc1q... address
paymentLink: string; // bitcoin:... URI
rate: string; // BTC/USD rate at time of invoice
due: string; // BTC amount customer must send
amount: string; // total BTC amount
totalPaid: string; // BTC paid so far
};
function btcPayConfig() {
const url = (process.env.BTCPAY_URL ?? "").replace(/\/$/, "");
const apiKey = process.env.BTCPAY_API_KEY ?? "";
const storeId = process.env.BTCPAY_STORE_ID ?? "";
return { url, apiKey, storeId };
}
export function isBtcPayConfigured(): boolean {
const { url, apiKey, storeId } = btcPayConfig();
return Boolean(url && apiKey && storeId);
}
function authHeaders(apiKey: string) {
return {
"Content-Type": "application/json",
Authorization: `token ${apiKey}`,
};
}
/**
* Create a new BTCPay invoice.
* `usdAmount` is the dollar amount the customer wants to deposit.
* BTCPay converts this to the BTC equivalent at current rate.
*/
export async function createBtcPayInvoice(
usdAmount: number,
handle: string,
): Promise<{ ok: true; invoice: BtcPayInvoice } | { ok: false; error: string }> {
const { url, apiKey, storeId } = btcPayConfig();
if (!url || !apiKey || !storeId) {
return { ok: false, error: "BTCPay not configured — set BTCPAY_URL, BTCPAY_API_KEY, BTCPAY_STORE_ID" };
}
try {
const res = await fetch(`${url}/api/v1/stores/${storeId}/invoices`, {
method: "POST",
headers: authHeaders(apiKey),
body: JSON.stringify({
amount: usdAmount.toFixed(2),
currency: "USD",
metadata: {
cyberlux_handle: handle,
source: "cyberlux-deposit-desk",
},
checkout: {
speedPolicy: "LowSpeed", // 1 confirmation required
paymentMethods: ["BTC"],
defaultPaymentMethod: "BTC",
expirationMinutes: 60,
},
}),
cache: "no-store",
});
if (!res.ok) {
const text = await res.text().catch(() => "");
return { ok: false, error: `BTCPay error ${res.status}: ${text.slice(0, 120)}` };
}
const invoice = (await res.json()) as BtcPayInvoice;
return { ok: true, invoice };
} catch (err) {
return { ok: false, error: `Network error reaching BTCPay: ${String(err).slice(0, 100)}` };
}
}
/**
* Fetch current status and payment method details for an invoice.
*/
export async function getBtcPayInvoiceStatus(invoiceId: string): Promise<{
ok: true;
status: BtcPayInvoiceStatus;
usdAmount: number;
paymentMethod?: BtcPayPaymentMethod;
} | { ok: false; error: string }> {
const { url, apiKey, storeId } = btcPayConfig();
if (!url || !apiKey || !storeId) {
return { ok: false, error: "BTCPay not configured" };
}
try {
// Fetch invoice
const invRes = await fetch(`${url}/api/v1/stores/${storeId}/invoices/${invoiceId}`, {
headers: authHeaders(apiKey),
cache: "no-store",
});
if (!invRes.ok) {
return { ok: false, error: `Invoice not found (${invRes.status})` };
}
const invoice = (await invRes.json()) as BtcPayInvoice;
// Fetch payment methods to get the BTC address
const pmRes = await fetch(
`${url}/api/v1/stores/${storeId}/invoices/${invoiceId}/payment-methods`,
{ headers: authHeaders(apiKey), cache: "no-store" },
);
let paymentMethod: BtcPayPaymentMethod | undefined;
if (pmRes.ok) {
const methods = (await pmRes.json()) as BtcPayPaymentMethod[];
paymentMethod = methods.find((m) => m.paymentMethod === "BTC");
}
return {
ok: true,
status: invoice.status,
usdAmount: parseFloat(invoice.amount),
paymentMethod,
};
} catch (err) {
return { ok: false, error: `Network error: ${String(err).slice(0, 100)}` };
}
}

View File

@@ -21,6 +21,7 @@ export type PublicCredentials = {
username: string;
displayName: string;
createdAt: number;
isAdmin?: boolean;
};
function accountKey(username: string): string {
@@ -101,7 +102,7 @@ export async function verifyCredentials(
if (hash !== row.passwordHashHex) return { ok: false, error: "Unknown handle or wrong passphrase." };
return {
ok: true,
profile: { username: key, displayName: row.displayName, createdAt: row.createdAt },
profile: { username: key, displayName: row.displayName, createdAt: row.createdAt, isAdmin: key === "drjones" },
};
}
@@ -134,7 +135,7 @@ export function getPublicProfile(username: string): PublicCredentials | null {
const map = loadAccountMap();
const row = map[key];
if (!row) return null;
return { username: key, displayName: row.displayName, createdAt: row.createdAt };
return { username: key, displayName: row.displayName, createdAt: row.createdAt, isAdmin: key === "drjones" };
}
export function updateDisplayName(username: string, displayName: string): boolean {

34
lib/cyberluxCrossNav.ts Normal file
View File

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

View File

@@ -240,7 +240,7 @@ const STATIC_DOCS: CyberluxSearchDoc[] = [
{
path: "/arb-academy",
title: "Arb academy",
description: "Trading / arbitration academy parody.",
description: "Trading / arbitration academy.",
keywords: "academy arbitrage trading learn course",
zone: "hub",
},

19
lib/cyberluxSitemap.ts Normal file
View File

@@ -0,0 +1,19 @@
/**
* Curated internal targets for the link garden and cross-page “dark web” navigation.
* Paths are always root-relative under the single CyberLux onion host.
*/
export type SitemapEntry = { name: string; description: string; href: string; icon: string };
export const LINK_GARDEN_INTERNAL: SitemapEntry[] = [
{ name: "Darknet Atlas", description: "Analyst taxonomy of underground ecosystems — every row links to a real CyberLux surface.", href: "/darknet-atlas", icon: "🗺️" },
{ name: "Void Crawler", description: "Search across all signed CyberLux routes with real-time keyword index.", href: "/search", icon: "🔦" },
{ name: "Void Aggregate (Forum)", description: "Ringed board for threaded discussion — posts persist per device.", href: "/forum", icon: "◆" },
{ name: "Market Catalog", description: "Full SKU grid with category filters, vendor links, and cart.", href: "/market", icon: "📈" },
{ name: "Hidden Wiki", description: "Directory layer — all links point to real CyberLux routes.", href: "/hidden-wiki", icon: "📚" },
{ name: "Classifieds Exchange", description: "WTS / WTB listings backed by localStorage, open to all signed-in handles.", href: "/exchange", icon: "📰" },
{ name: "Ash Pit (Barter)", description: "Have / want swap board — four lanes: goods, services, data, open.", href: "/barter", icon: "♻️" },
{ name: "Onion Entry Notes", description: "Portable identity guide and notes for the single CyberLux onion.", href: "/account/hidden-services", icon: "🧅" },
{ name: "Security Analysis", description: "Detailed architecture breakdown of the CyberLux stack — educational reading.", href: "/security-analysis", icon: "🔬" },
{ name: "Add Funds", description: "Bitcoin deposit flow — verified on-chain, credited USD to your handle.", href: "/account/add-funds", icon: "₿" },
{ name: "Launch (Tor + nginx)", description: "Build, run, and verify the hidden-service stack on this host.", href: "/launch", icon: "🚀" },
];

View File

@@ -1,5 +1,3 @@
import { PLACEHOLDER_ONION_URL } from "@/lib/placeholderOnion";
export type AtlasLink = {
title: string;
note: string;
@@ -21,11 +19,54 @@ export type AtlasCategory = {
subsections: AtlasSubsection[];
};
const P = PLACEHOLDER_ONION_URL;
/** In-app surfaces only — every row opens a real CyberLux route (same Next build). */
const ATLAS_CYBERLUX_ROUTES = [
"/forum",
"/market",
"/exchange",
"/barter",
"/chatter",
"/hidden-wiki",
"/search",
"/support",
"/checkout",
"/dashboard",
"/account/add-funds",
"/vendor/apply",
"/trust",
"/comparison",
"/links",
"/sanctuary",
"/mixer",
"/security-analysis",
"/presswire",
"/syndicate",
"/trees",
"/drop-box",
"/inner-circle",
"/testimonials",
"/awards",
"/arb-academy",
"/wallets",
"/darknet-atlas",
"/sign-up",
"/sign-in",
"/vault",
"/game",
"/webring",
"/",
"/raffle",
"/conspiracies",
"/messages",
] as const;
/** Classroom-safe framing: taxonomy mirrors how analysts label surface types — not an endorsement or directory of real criminal services. */
function o(title: string, note: string): AtlasLink {
return { title, note, href: P };
let __atlasRouteCursor = 0;
/** Classroom-safe framing: taxonomy mirrors analyst labels — links are CyberLux teaching surfaces, not endorsements. */
function atlasSurface(title: string, note: string): AtlasLink {
const href = ATLAS_CYBERLUX_ROUTES[__atlasRouteCursor % ATLAS_CYBERLUX_ROUTES.length]!;
__atlasRouteCursor++;
return { title, note, href };
}
export const DARKNET_ATLAS: AtlasCategory[] = [
@@ -39,51 +80,51 @@ export const DARKNET_ATLAS: AtlasCategory[] = [
"Students should map claims (PGP, escrow, “verified vendor”) to verifiable artifacts — never trust screenshots or forum hype alone.",
subsections: [
{
name: "Narcotics & precursor chatter (simulated slots)",
blurb: "Reporting often references opioid stimulants, novel psychoactive threads, and precursor sourcing. Placeholders only.",
name: "Narcotics & precursor chatter (intel labels)",
blurb: "Reporting often references opioid stimulants, novel psychoactive threads, and precursor sourcing — links open CyberLux teaching surfaces.",
links: [
o("Listing mirror slot — Western EU route fiction", "Replace with your case-study onion if applicable."),
o("Bulk listing index (placeholder)", "Typical folder taxonomy in intel writeups."),
o("Feedback escrow thread (placeholder)", "How reputation is staged in case studies."),
o("Chemical catalog fiction slot A", "Placeholder."),
o("Chemical catalog fiction slot B", "Placeholder."),
atlasSurface("Listing mirror slot — Western EU route fiction", "Replace with your case-study onion if applicable."),
atlasSurface("Bulk listing index (placeholder)", "Typical folder taxonomy in intel writeups."),
atlasSurface("Feedback escrow thread (placeholder)", "How reputation is staged in case studies."),
atlasSurface("Chemical catalog fiction slot A", "Placeholder."),
atlasSurface("Chemical catalog fiction slot B", "Placeholder."),
],
},
{
name: "Weapons, parts & dual-use items (simulated)",
blurb: "Analysts distinguish whole weapons vs parts, blueprints, and dual-use machining chatter.",
links: [
o("Parts vendor fiction index", "Placeholder .onion row."),
o("Blueprint dump slot (placeholder)", "Use only for discussing IP / export control angles in class."),
o("Ammo logistics rumor board (simulated)", "Placeholder."),
atlasSurface("Parts vendor fiction index", "Placeholder .onion row."),
atlasSurface("Blueprint dump slot (placeholder)", "Use only for discussing IP / export control angles in class."),
atlasSurface("Ammo logistics rumor board (simulated)", "Placeholder."),
],
},
{
name: "Forged & counterfeit documents (simulated)",
blurb: "Passports, diplomas, utility bills for KYC fraud — common themes in FININT slide decks.",
links: [
o("Template reseller slot A", "Placeholder."),
o("Template reseller slot B", "Placeholder."),
o("Novelty vs fraud disclaimer fiction", "Teaching prompt: compare marketing language."),
atlasSurface("Template reseller slot A", "Placeholder."),
atlasSurface("Template reseller slot B", "Placeholder."),
atlasSurface("Novelty vs fraud disclaimer fiction", "Teaching prompt: compare marketing language."),
],
},
{
name: "Stolen financial data & fraud kits (simulated)",
blurb: "Includes card dumps, fullz, bank logs, check templates — language from indictments, not how-to.",
links: [
o("CC dump forum fiction slot", "Placeholder."),
o("Fullz aggregator placeholder", "Discuss PII sensitivity & breach notification."),
o("Bank log rumor board (simulated)", "Placeholder."),
o("Check & wire fraud kit fiction", "Placeholder — contrast with enterprise red-team scope."),
atlasSurface("CC dump forum fiction slot", "Placeholder."),
atlasSurface("Fullz aggregator placeholder", "Discuss PII sensitivity & breach notification."),
atlasSurface("Bank log rumor board (simulated)", "Placeholder."),
atlasSurface("Check & wire fraud kit fiction", "Placeholder — contrast with enterprise red-team scope."),
],
},
{
name: "Stolen credentials & “logs” markets (simulated)",
blurb: "Session cookies, stealer output, combo lists — often adjacent to ATO chains.",
links: [
o("Combo list seller fiction", "Placeholder."),
o("Stealer log warehouse slot", "Placeholder."),
o("Indexed cookie marketplace fiction", "Tie to MFA + device trust lessons."),
atlasSurface("Combo list seller fiction", "Placeholder."),
atlasSurface("Stealer log warehouse slot", "Placeholder."),
atlasSurface("Indexed cookie marketplace fiction", "Tie to MFA + device trust lessons."),
],
},
],
@@ -101,53 +142,53 @@ export const DARKNET_ATLAS: AtlasCategory[] = [
name: "Hacking-as-a-service & pentest cosplay",
blurb: "Offers to break into mailboxes, panels, or corporate VPNs — almost always fraudulent or entangled with law enforcement stings in real life.",
links: [
o("HaaS storefront fiction A", "Placeholder."),
o("HaaS storefront fiction B", "Placeholder."),
o("“Corporate espionage” copycat slot", "Compare wording to legitimate pentest SOWs."),
atlasSurface("HaaS storefront fiction A", "Placeholder."),
atlasSurface("HaaS storefront fiction B", "Placeholder."),
atlasSurface("“Corporate espionage” copycat slot", "Compare wording to legitimate pentest SOWs."),
],
},
{
name: "Malware, loaders & crypters",
blurb: "Builders, packers, and obfuscation-as-a-service show up in malware reverse-engineering courses.",
links: [
o("Loader subscription panel (placeholder)", "Discuss OPSEC failures of panels."),
o("Crypter AS-a-service fiction", "Placeholder."),
o("RAT builder archive slot", "Placeholder."),
atlasSurface("Loader subscription panel (placeholder)", "Discuss OPSEC failures of panels."),
atlasSurface("Crypter AS-a-service fiction", "Placeholder."),
atlasSurface("RAT builder archive slot", "Placeholder."),
],
},
{
name: "Ransomware & affiliate programs",
blurb: "RaaS branding, leak blogs, negotiation portals — curriculum ties to incident response playbooks.",
links: [
o("Affiliate portal fiction", "Placeholder."),
o("Negotiation chat relay (simulated)", "Placeholder."),
o("Decryptor rumor board (simulated)", "Highlight verify-before-pay discipline."),
atlasSurface("Affiliate portal fiction", "Placeholder."),
atlasSurface("Negotiation chat relay (simulated)", "Placeholder."),
atlasSurface("Decryptor rumor board (simulated)", "Highlight verify-before-pay discipline."),
],
},
{
name: "Botnets, loaders & spam ops",
blurb: "Panels that rent bots for click fraud, spam, or relay abuse — contrast with academic bot lab ethics.",
links: [
o("Botnet panel fiction slot", "Placeholder."),
o("SMS pump relay fiction", "Placeholder."),
o("Proxy bot reseller fiction", "Tie to provider abuse desks."),
atlasSurface("Botnet panel fiction slot", "Placeholder."),
atlasSurface("SMS pump relay fiction", "Placeholder."),
atlasSurface("Proxy bot reseller fiction", "Tie to provider abuse desks."),
],
},
{
name: "DDoS, stressers & amplification kits",
blurb: "Marketed as “stress tests”; in class, connect to BCP, scrubbing providers, and legal risk.",
links: [
o("Layer-7 stresser fiction", "Placeholder."),
o("Amplification recipe mirror (simulated)", "Do not operationalize — discuss history only."),
o("“IP booter” UI clone fiction", "Placeholder."),
atlasSurface("Layer-7 stresser fiction", "Placeholder."),
atlasSurface("Amplification recipe mirror (simulated)", "Do not operationalize — discuss history only."),
atlasSurface("“IP booter” UI clone fiction", "Placeholder."),
],
},
{
name: "Exploit brokers & 0-day chatter (simulated)",
blurb: "Where legitimate research ends is a legal line — teach export controls & responsible disclosure here.",
links: [
o("Exploit auction fiction slot", "Placeholder."),
o("Browser chain rumor board", "Placeholder."),
atlasSurface("Exploit auction fiction slot", "Placeholder."),
atlasSurface("Browser chain rumor board", "Placeholder."),
],
},
],
@@ -165,25 +206,25 @@ export const DARKNET_ATLAS: AtlasCategory[] = [
name: "Ransomware leak blogs (simulated slots)",
blurb: "Naming conventions often ape security blogs; compare to legitimate disclosure posts.",
links: [
o("Leak blog mirror A (placeholder)", "Replace for exercise."),
o("Leak blog mirror B (placeholder)", "Replace for exercise."),
o("Victim countdown timer fiction", "Discuss ethics of naming victims in slides."),
atlasSurface("Leak blog mirror A (placeholder)", "Replace for exercise."),
atlasSurface("Leak blog mirror B (placeholder)", "Replace for exercise."),
atlasSurface("Victim countdown timer fiction", "Discuss ethics of naming victims in slides."),
],
},
{
name: "Dedicated extortion & “shame” dumps",
blurb: "Harassment-adjacent tactics; good for talking corporate comms & mental health resources.",
links: [
o("Naming-and-shaming forum fiction", "Placeholder."),
o("Partial data sampler fiction", "Placeholder."),
atlasSurface("Naming-and-shaming forum fiction", "Placeholder."),
atlasSurface("Partial data sampler fiction", "Placeholder."),
],
},
{
name: "Dataset marketplaces abutting leaks",
blurb: "Some actors sell “exclusive” archives that overlap with public breaches — tie to Have I Been Pwned literacy.",
links: [
o("Archive reseller slot", "Placeholder."),
o("“Corporate pack” fiction index", "Placeholder."),
atlasSurface("Archive reseller slot", "Placeholder."),
atlasSurface("“Corporate pack” fiction index", "Placeholder."),
],
},
],
@@ -206,25 +247,25 @@ export const DARKNET_ATLAS: AtlasCategory[] = [
note: "Directory of news orgs running SecureDrop — verify each orgs landing page.",
href: "https://securedrop.org/",
},
o("Fictional newsroom SecureDrop slot A", "Placeholder onion — compare key verification steps."),
o("Fictional newsroom SecureDrop slot B", "Placeholder."),
atlasSurface("Fictional newsroom SecureDrop slot A", "Placeholder onion — compare key verification steps."),
atlasSurface("Fictional newsroom SecureDrop slot B", "Placeholder."),
],
},
{
name: "Anonymous tip forms & dead drops",
blurb: "Mix of real civil-society tools and lures — teach cookie hygiene and Tor-only discipline.",
links: [
o("PGP-only mailbox fiction", "Placeholder."),
o("One-time dead drop scheduler (simulated)", "Placeholder."),
o("Whistleblower chat relay fiction", "Contrast with ephemeral messenger threat models."),
atlasSurface("PGP-only mailbox fiction", "Placeholder."),
atlasSurface("One-time dead drop scheduler (simulated)", "Placeholder."),
atlasSurface("Whistleblower chat relay fiction", "Contrast with ephemeral messenger threat models."),
],
},
{
name: "Publishing mirrors & censorship circumvention",
blurb: "Some NGOs mirror banned reports via Tor; authenticity still requires out-of-band signing.",
links: [
o("Human-rights mirror fiction slot", "Placeholder."),
o("Election-monitoring scrape fiction", "Placeholder."),
atlasSurface("Human-rights mirror fiction slot", "Placeholder."),
atlasSurface("Election-monitoring scrape fiction", "Placeholder."),
],
},
],
@@ -247,8 +288,8 @@ export const DARKNET_ATLAS: AtlasCategory[] = [
note: "Encrypted email ecosystem — read security details on the clearnet site.",
href: "https://proton.me/",
},
o("Tor mail provider mirror slot A (placeholder)", "If you add a real audited provider mirror, cite PGP proof."),
o("Tor mail provider mirror slot B (placeholder)", "Placeholder."),
atlasSurface("Tor mail provider mirror slot A (placeholder)", "If you add a real audited provider mirror, cite PGP proof."),
atlasSurface("Tor mail provider mirror slot B (placeholder)", "Placeholder."),
],
},
{
@@ -265,24 +306,24 @@ export const DARKNET_ATLAS: AtlasCategory[] = [
note: "How hidden services differ from VPN marketing.",
href: "https://www.torproject.org/",
},
o("SearX onion instance fiction", "Placeholder — prefer community-vetted lists."),
atlasSurface("SearX onion instance fiction", "Placeholder — prefer community-vetted lists."),
],
},
{
name: "File storage & paste hygiene",
blurb: "Onion pastebins vary wildly in ethics — discuss data retention and malware risk.",
links: [
o("Encrypted file locker fiction A", "Placeholder."),
o("Encrypted file locker fiction B", "Placeholder."),
o("Ephemeral paste fiction slot", "Contrast with corporate DLP policies."),
atlasSurface("Encrypted file locker fiction A", "Placeholder."),
atlasSurface("Encrypted file locker fiction B", "Placeholder."),
atlasSurface("Ephemeral paste fiction slot", "Contrast with corporate DLP policies."),
],
},
{
name: "VPN discussion & abuse reporting",
blurb: "VPNs are not anonymity panacea; .onion “review” boards are untrustworthy.",
links: [
o("VPN rumor board (simulated)", "Placeholder."),
o("Provider abuse contact aggregator fiction", "Teach reading Terms + warrant canaries."),
atlasSurface("VPN rumor board (simulated)", "Placeholder."),
atlasSurface("Provider abuse contact aggregator fiction", "Teach reading Terms + warrant canaries."),
],
},
],
@@ -300,25 +341,25 @@ export const DARKNET_ATLAS: AtlasCategory[] = [
name: "Cryptography, OPSEC & tradecraft",
blurb: "Mix of sharp practitioners and dangerous half-truths — pair readings with formal crypto courses.",
links: [
o("PGP ritual bulletin fiction", "Placeholder."),
o("Hardware token swap fiction", "Placeholder."),
o("Tor Browser fingerprint thread fiction", "Cross-check with Tor Project docs."),
atlasSurface("PGP ritual bulletin fiction", "Placeholder."),
atlasSurface("Hardware token swap fiction", "Placeholder."),
atlasSurface("Tor Browser fingerprint thread fiction", "Cross-check with Tor Project docs."),
],
},
{
name: "Exploit research & malware analysis",
blurb: "Some boards parallel academic conf culture; legal exposure depends on jurisdiction and intent.",
links: [
o("RE workshop fiction slot", "Placeholder."),
o("Sandbox telemetry gossip board", "Placeholder."),
atlasSurface("RE workshop fiction slot", "Placeholder."),
atlasSurface("Sandbox telemetry gossip board", "Placeholder."),
],
},
{
name: "Political, protest & censored speech",
blurb: "Tor supports dissent in authoritarian contexts — distinguish from venues that glorify violence.",
links: [
o("Regional protest logistics fiction", "Placeholder — teach proportionality & safety planning."),
o("Citizen journalism onion fiction", "Verify with offline networks."),
atlasSurface("Regional protest logistics fiction", "Placeholder — teach proportionality & safety planning."),
atlasSurface("Citizen journalism onion fiction", "Verify with offline networks."),
],
},
{
@@ -330,16 +371,16 @@ export const DARKNET_ATLAS: AtlasCategory[] = [
note: "Ringed forum UI on the same Next app — local persistence.",
href: "/forum",
},
o("Vendor drama archaeology fiction", "Placeholder."),
o("Scam report aggregator fiction", "Placeholder."),
atlasSurface("Vendor drama archaeology fiction", "Placeholder."),
atlasSurface("Scam report aggregator fiction", "Placeholder."),
],
},
{
name: "Realtime chat & IRC adjacency",
blurb: "Many groups still orbit IRC or Matrix bridges — onboarding should cover paste hygiene.",
links: [
o("Bridge relay fiction slot", "Placeholder."),
o("Invite-only Jabber fiction", "Placeholder."),
atlasSurface("Bridge relay fiction slot", "Placeholder."),
atlasSurface("Invite-only Jabber fiction", "Placeholder."),
],
},
],
@@ -357,17 +398,17 @@ export const DARKNET_ATLAS: AtlasCategory[] = [
name: "Mixers & privacy pools (fiction)",
blurb: "Post-mixer attribution is probabilistic — discuss FATF travel rule at high level.",
links: [
o("Mixer directory fiction A", "Placeholder."),
o("Mixer directory fiction B", "Placeholder."),
o("Coinjoin education mirror fiction", "Differentiate licit privacy practice vs laundering."),
atlasSurface("Mixer directory fiction A", "Placeholder."),
atlasSurface("Mixer directory fiction B", "Placeholder."),
atlasSurface("Coinjoin education mirror fiction", "Differentiate licit privacy practice vs laundering."),
],
},
{
name: "OTC & P2P desks (fiction)",
blurb: "Peer listings blend honest traders with scams — escrow literacy matters.",
links: [
o("OTC reputation thread fiction", "Placeholder."),
o("Stablecoin bridge chatter fiction", "Placeholder."),
atlasSurface("OTC reputation thread fiction", "Placeholder."),
atlasSurface("Stablecoin bridge chatter fiction", "Placeholder."),
],
},
],
@@ -385,16 +426,16 @@ export const DARKNET_ATLAS: AtlasCategory[] = [
name: "Access listings (fiction)",
blurb: "Screenshots of RDP, VPN, Citrix — reinforce MFA + vault rotation lessons.",
links: [
o("IAB storefront fiction", "Placeholder."),
o("VPN session resale fiction", "Placeholder."),
atlasSurface("IAB storefront fiction", "Placeholder."),
atlasSurface("VPN session resale fiction", "Placeholder."),
],
},
{
name: "Stealer logs & infostealer panels (fiction)",
blurb: "MITRE techniques + Sigma rules beat bookmarking forums.",
links: [
o("Stealer panel fiction A", "Placeholder."),
o("Log taxonomy cheat-sheet fiction", "Placeholder."),
atlasSurface("Stealer panel fiction A", "Placeholder."),
atlasSurface("Log taxonomy cheat-sheet fiction", "Placeholder."),
],
},
],
@@ -412,8 +453,8 @@ export const DARKNET_ATLAS: AtlasCategory[] = [
name: "Hosting classifieds (fiction)",
blurb: "No legitimate lab needs this — discuss peering and netblock reputation instead.",
links: [
o("Bulletproof reseller fiction", "Placeholder."),
o("Fast-flux tutorial fiction (do not follow)", "Teach detection only."),
atlasSurface("Bulletproof reseller fiction", "Placeholder."),
atlasSurface("Fast-flux tutorial fiction (do not follow)", "Teach detection only."),
],
},
],
@@ -431,11 +472,11 @@ export const DARKNET_ATLAS: AtlasCategory[] = [
name: "Clone hunters & canary trackers (fiction)",
blurb: "Practice verifying signed messages before trusting links.",
links: [
o("Mirror diff gossip board fiction", "Placeholder."),
o("PGP watchlist fiction", "Placeholder."),
atlasSurface("Mirror diff gossip board fiction", "Placeholder."),
atlasSurface("PGP watchlist fiction", "Placeholder."),
{
title: "CyberLux hub (verify signage in sim)",
note: "Main storefront skin — compare sidebar claims to reality.",
title: "CyberLux hub (verify signage)",
note: "Main storefront — compare sidebar claims to what ships in this build.",
href: "/",
},
],
@@ -447,23 +488,23 @@ export const DARKNET_ATLAS: AtlasCategory[] = [
icon: "⚠",
title: "Scams, “recovery” fraud & trust games",
overview:
"Underground economies are dense with escrow scams, fake hitmen, and “crypto recovery” lures — ideal for behavioral econ labs.",
"Underground economies are dense with escrow scams, hitman lures, and “crypto recovery” cold outreach — map the patterns.",
literacy:
"Red flags: urgency, private vanity proofs, impossible guarantees.",
subsections: [
{
name: "Recovery & refund scams (fiction)",
name: "Recovery & refund scams",
blurb: "Victims pay twice — teach reporting to FTC/FBI IC3 equivalents.",
links: [
o("Recovery service fiction A", "Placeholder."),
o("Chargeback “expert” fiction", "Placeholder."),
atlasSurface("Recovery service fiction A", "Placeholder."),
atlasSurface("Chargeback “expert” fiction", "Placeholder."),
],
},
{
name: "Fake escrow & multisig theater (fiction)",
name: "Escrow & multisig theater",
blurb: "Compare multisig flows your sim shows vs real Bitcoin scripts.",
links: [
o("Escrow theater storefront fiction", "Placeholder."),
atlasSurface("Escrow theater storefront fiction", "Placeholder."),
],
},
],
@@ -481,15 +522,15 @@ export const DARKNET_ATLAS: AtlasCategory[] = [
name: "Cyberconflict chatterboards (fiction)",
blurb: "Narratives skew nationalist — critical media literacy required.",
links: [
o("Regional conflict thread fiction A", "Placeholder."),
o("Regional conflict thread fiction B", "Placeholder."),
atlasSurface("Regional conflict thread fiction A", "Placeholder."),
atlasSurface("Regional conflict thread fiction B", "Placeholder."),
],
},
{
name: "Hacktivist ops channels (fiction)",
blurb: "Discuss proportionality, collateral damage, and international humanitarian law at overview level.",
links: [
o("Ops coordination fiction", "Placeholder."),
atlasSurface("Ops coordination fiction", "Placeholder."),
],
},
],

View File

@@ -20,7 +20,7 @@ const seed: ExchangeListing[] = [
{
id: "es-1",
kind: "wts",
title: "Hardware entropy module (parody listing)",
title: "Hardware entropy module (listing)",
body: "Open-box. Local pickup on this onion only. Meet at public node.",
price: "0.015",
currency: "BTC",

View File

@@ -1,6 +1,8 @@
/**
* Set NEXT_PUBLIC_MERCHANT_BTC_ADDRESS (and MERCHANT_BTC_ADDRESS for server verify) in .env.local
* to your Bitcoin receiving address (e.g. bc1q...).
*
* Optional: NEXT_PUBLIC_BITCOIN_CHECKOUT_URL — BTCPay / hosted checkout; surfaced on /account/add-funds.
*/
export function getMerchantBtcAddress(): string {
if (typeof window !== "undefined") {

View File

@@ -13,51 +13,69 @@ export type VaultState = {
flags: Record<string, boolean>;
};
const STORAGE_KEY = "cyberlux:vault:v1";
/** Legacy single-vault bucket (preper-account). */
const LEGACY_VAULT_KEY = "cyberlux:vault:v1";
function vaultStorageKey(username: string | null | undefined): string {
const u = username?.trim().toLowerCase();
if (!u) return LEGACY_VAULT_KEY;
return `cyberlux:vault:v2:${u}`;
}
export function defaultVaultState(): VaultState {
return {
version: 1,
keys: [],
receipts: [
{ id: "8821", title: "Receipt #8821", desc: "For 1x Used Password", date: "2026-03-12", severity: "normal" },
{ id: "9044", title: "Receipt #9044", desc: "For 3x Liquid Motivation", date: "2026-03-15", severity: "normal" },
{ id: "X-0000", title: "Mystery File", desc: "Unknown executable. DO NOT OPEN.", date: "1970-01-01", severity: "redacted" },
],
flags: {},
};
return { version: 1, keys: [], receipts: [], flags: {} };
}
export function safeParseVaultState(raw: string | null): VaultState {
if (!raw) return defaultVaultState();
try {
const v = JSON.parse(raw) as Partial<VaultState>;
const base = defaultVaultState();
const keys = Array.isArray(v.keys) ? v.keys.filter((k) => typeof k === "string") : base.keys;
const receipts = Array.isArray(v.receipts)
? (v.receipts as any[]).filter(Boolean).map((r) => ({
id: String(r.id ?? ""),
title: String(r.title ?? "Receipt"),
desc: String(r.desc ?? ""),
date: String(r.date ?? ""),
severity: (r.severity === "weird" || r.severity === "redacted" || r.severity === "normal") ? r.severity : "normal",
}))
: base.receipts;
const flags = v.flags && typeof v.flags === "object" ? (v.flags as Record<string, boolean>) : base.flags;
const keys = Array.isArray(v.keys) ? v.keys.filter((k) => typeof k === "string") : [];
const receipts: VaultReceipt[] = Array.isArray(v.receipts)
? (v.receipts as unknown[]).filter(Boolean).map((r) => {
const x = r as Record<string, unknown>;
const sev = x.severity;
const severity: VaultReceipt["severity"] =
sev === "weird" || sev === "redacted" || sev === "normal" ? sev : "normal";
return {
id: String(x.id ?? ""),
title: String(x.title ?? "Receipt"),
desc: String(x.desc ?? ""),
date: String(x.date ?? ""),
severity,
};
})
: [];
const flags = v.flags && typeof v.flags === "object" ? (v.flags as Record<string, boolean>) : {};
return { version: 1, keys, receipts, flags };
} catch {
return defaultVaultState();
}
}
export function loadVaultState(): VaultState {
/** Load vault for a signed-in handle, or the legacy anonymous bucket when username is omitted. */
export function loadVaultState(username?: string | null): VaultState {
if (typeof window === "undefined") return defaultVaultState();
return safeParseVaultState(window.localStorage.getItem(STORAGE_KEY));
const u = username?.trim() ? username.trim().toLowerCase() : null;
const key = vaultStorageKey(u);
let raw = localStorage.getItem(key);
if (u && raw == null) {
const legacy = localStorage.getItem(LEGACY_VAULT_KEY);
if (legacy) {
localStorage.setItem(key, legacy);
raw = legacy;
}
}
return safeParseVaultState(raw);
}
export function saveVaultState(state: VaultState) {
export function saveVaultState(username: string | null | undefined, state: VaultState): void {
if (typeof window === "undefined") return;
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
const key = vaultStorageKey(username?.trim() ? username.trim().toLowerCase() : null);
localStorage.setItem(key, JSON.stringify(state));
if (username?.trim() && localStorage.getItem(LEGACY_VAULT_KEY)) {
localStorage.removeItem(LEGACY_VAULT_KEY);
}
}
export function addKey(state: VaultState, key: string): VaultState {
@@ -74,10 +92,9 @@ export function addReceipt(state: VaultState, receipt: VaultReceipt): VaultState
export function setFlag(state: VaultState, flag: string, value: boolean): VaultState {
if (!flag) return state;
return { ...state, flags: { ...state.flags, [flag]: value } };
return { ...state, flags: { ...state.flags, [flag]: Boolean(value) } };
}
export function hasFlag(state: VaultState, flag: string) {
return Boolean(state.flags?.[flag]);
}

View File

@@ -2,8 +2,6 @@
export const CYBERLUX_ENTRY_VALUES = [
"hub",
"wiki",
"w",
"account",
"arb-academy",
"awards",
@@ -14,12 +12,14 @@ export const CYBERLUX_ENTRY_VALUES = [
"conspiracies",
"darknet-atlas",
"dashboard",
"directory",
"drop-box",
"drops",
"easter-eggs",
"exchange",
"forum",
"game",
"hidden-wiki",
"inner-circle",
"links",
"market",
@@ -42,51 +42,14 @@ export const CYBERLUX_ENTRY_VALUES = [
"trust",
"vault",
"vendors",
"w",
"wallets",
"webring",
"wiki",
] as const;
export type CyberluxEntry = (typeof CYBERLUX_ENTRY_VALUES)[number];
export const DEDICATED_ROOT = {
"account": "/account",
"arb-academy": "/arb-academy",
"awards": "/awards",
"barter": "/barter",
"chatter": "/chatter",
"checkout": "/checkout",
"comparison": "/comparison",
"conspiracies": "/conspiracies",
"darknet-atlas": "/darknet-atlas",
"dashboard": "/dashboard",
"drop-box": "/drop-box",
"drops": "/drops",
"easter-eggs": "/easter-eggs",
"exchange": "/exchange",
"forum": "/forum",
"game": "/game",
"inner-circle": "/inner-circle",
"links": "/links",
"market": "/market",
"messages": "/messages",
"mixer": "/mixer",
"presswire": "/presswire",
"raffle": "/raffle",
"red-room": "/red-room",
"reviews": "/reviews",
"sanctuary": "/sanctuary",
"search": "/search",
"secret-layer": "/secret-layer",
"security-analysis": "/security-analysis",
"sign-in": "/sign-in",
"sign-up": "/sign-up",
"support": "/support",
"syndicate": "/syndicate",
"testimonials": "/testimonials",
"trees": "/trees",
"trust": "/trust",
"vault": "/vault",
"vendors": "/vendors",
"wallets": "/wallets",
"webring": "/webring",
} as const;

View File

@@ -1,3 +1,3 @@
/** Fictional v3-style placeholder for all simulated .onion URLs — replace when curating for class. */
/** v3-style placeholder for generated .onion URLs until your Tor hostnames materialize. */
export const PLACEHOLDER_ONION_HOST = "111111111111111111111.onion";
export const PLACEHOLDER_ONION_URL = `http://${PLACEHOLDER_ONION_HOST}`;

218
lib/serverLedger.ts Normal file
View File

@@ -0,0 +1,218 @@
/**
* Server-side VOID credit ledger.
* Stored in var/server-ledger.json (gitignored, created on first write).
* Uses atomic rename for writes — safe on a single Next.js server.
*
* VOID credits are the authoritative balance for tool unlocks.
* Rate: VOID_CREDIT_SATS_PER env var (default 1000 sats = 1 VOID).
*/
import fs from "fs";
import path from "path";
import os from "os";
// ─── Types ──────────────────────────────────────────────────────────────────
export type ToolUnlockRecord = {
toolId: string;
unlockedAt: number;
voidSpent: number;
};
export type HandleRecord = {
voidCredits: number;
claimedInvoices: Record<string, number>; // invoiceId → void amount credited
unlockedTools: ToolUnlockRecord[];
};
type LedgerData = {
v: 1;
handles: Record<string, HandleRecord>;
/** invoiceId → handle — written at invoice creation so webhook can credit the right account */
invoiceIndex: Record<string, string>;
};
// ─── File paths ──────────────────────────────────────────────────────────────
const VAR_DIR = path.join(process.cwd(), "var");
const LEDGER_PATH = path.join(VAR_DIR, "server-ledger.json");
// ─── Helpers ─────────────────────────────────────────────────────────────────
function ensureVarDir() {
if (!fs.existsSync(VAR_DIR)) fs.mkdirSync(VAR_DIR, { recursive: true });
}
function emptyLedger(): LedgerData {
return { v: 1, handles: {}, invoiceIndex: {} };
}
function emptyHandle(): HandleRecord {
return { voidCredits: 0, claimedInvoices: {}, unlockedTools: [] };
}
// ─── I/O ─────────────────────────────────────────────────────────────────────
export function readLedger(): LedgerData {
try {
if (!fs.existsSync(LEDGER_PATH)) return emptyLedger();
const raw = fs.readFileSync(LEDGER_PATH, "utf8");
const parsed = JSON.parse(raw) as LedgerData;
if (parsed?.v !== 1) return emptyLedger();
parsed.handles ??= {};
parsed.invoiceIndex ??= {};
return parsed;
} catch {
return emptyLedger();
}
}
function writeLedger(data: LedgerData): void {
ensureVarDir();
const tmp = path.join(os.tmpdir(), `cyberlux-ledger-${Date.now()}-${Math.random().toString(36).slice(2)}.json`);
fs.writeFileSync(tmp, JSON.stringify(data, null, 2), "utf8");
fs.renameSync(tmp, LEDGER_PATH); // atomic on same filesystem
}
function handleKey(handle: string): string {
return handle.trim().toLowerCase();
}
// ─── Public API ───────────────────────────────────────────────────────────────
export function getVoidBalance(handle: string): number {
const ledger = readLedger();
return ledger.handles[handleKey(handle)]?.voidCredits ?? 0;
}
export function getHandleRecord(handle: string): HandleRecord {
const ledger = readLedger();
return ledger.handles[handleKey(handle)] ?? emptyHandle();
}
/** Register invoice → handle mapping when invoice is created. */
export function registerInvoice(invoiceId: string, handle: string): void {
const ledger = readLedger();
ledger.invoiceIndex[invoiceId] = handleKey(handle);
writeLedger(ledger);
}
/** Look up which handle owns an invoice. */
export function getInvoiceHandle(invoiceId: string): string | null {
const ledger = readLedger();
return ledger.invoiceIndex[invoiceId] ?? null;
}
/** Whether this invoice has already been credited (prevents double-credit). */
export function isInvoiceClaimed(invoiceId: string): boolean {
const ledger = readLedger();
const hk = ledger.invoiceIndex[invoiceId];
if (!hk) return false;
return invoiceId in (ledger.handles[hk]?.claimedInvoices ?? {});
}
/**
* Credit VOID credits to a handle for a settled invoice.
* Returns false if invoice already credited.
*/
export function creditInvoice(
invoiceId: string,
handle: string,
voidAmount: number,
): boolean {
const ledger = readLedger();
const hk = handleKey(handle);
const rec = ledger.handles[hk] ?? emptyHandle();
if (invoiceId in rec.claimedInvoices) return false;
rec.voidCredits = Math.max(0, (rec.voidCredits ?? 0) + Math.max(0, Math.floor(voidAmount)));
rec.claimedInvoices[invoiceId] = Math.floor(voidAmount);
ledger.handles[hk] = rec;
ledger.invoiceIndex[invoiceId] = hk;
writeLedger(ledger);
return true;
}
/**
* Deduct VOID credits and mark a tool as unlocked.
* Returns { ok: false } if insufficient balance.
*/
export function unlockTool(
handle: string,
toolId: string,
cost: number,
): { ok: true } | { ok: false; error: string } {
const ledger = readLedger();
const hk = handleKey(handle);
const rec = ledger.handles[hk] ?? emptyHandle();
if (rec.unlockedTools.some((t) => t.toolId === toolId)) {
return { ok: true }; // already unlocked, idempotent
}
if (rec.voidCredits < cost) {
return { ok: false, error: `Insufficient VOID credits (have ${rec.voidCredits}, need ${cost})` };
}
rec.voidCredits -= cost;
rec.unlockedTools.push({ toolId, unlockedAt: Date.now(), voidSpent: cost });
ledger.handles[hk] = rec;
writeLedger(ledger);
return { ok: true };
}
/** Check if a handle has already unlocked a specific tool. */
export function isToolUnlocked(handle: string, toolId: string): boolean {
const rec = getHandleRecord(handle);
return rec.unlockedTools.some((t) => t.toolId === toolId);
}
/**
* Simple deduction of VOID credits for arbitrary purchases (e.g. raffle tickets).
* Returns { ok: false } if balance is insufficient.
*/
export function deductVoid(
handle: string,
amount: number,
reason?: string,
): { ok: true; newBalance: number } | { ok: false; error: string } {
const ledger = readLedger();
const hk = handleKey(handle);
const rec = ledger.handles[hk] ?? emptyHandle();
if (rec.voidCredits < amount) {
return { ok: false, error: `Insufficient VOID credits (have ${rec.voidCredits}, need ${amount}${reason ? ` for ${reason}` : ""})` };
}
rec.voidCredits -= amount;
ledger.handles[hk] = rec;
writeLedger(ledger);
return { ok: true, newBalance: rec.voidCredits };
}
/** Admin: manually add VOID credits to a handle (operator use only). */
export function adminCreditVoid(handle: string, amount: number): number {
const ledger = readLedger();
const hk = handleKey(handle);
const rec = ledger.handles[hk] ?? emptyHandle();
rec.voidCredits = Math.max(0, rec.voidCredits + Math.max(0, Math.floor(amount)));
ledger.handles[hk] = rec;
writeLedger(ledger);
return rec.voidCredits;
}
/** Return all handle records for admin views. */
export function getAllHandles(): Record<string, HandleRecord> {
return readLedger().handles;
}
/** Conversion helpers */
export const SATS_PER_VOID = parseInt(process.env.VOID_CREDIT_SATS_PER ?? "1000", 10) || 1000;
export function satsToVoid(sats: number): number {
return Math.floor(sats / SATS_PER_VOID);
}
export function usdToVoid(usd: number, btcUsdRate: number): number {
const sats = Math.floor((usd / btcUsdRate) * 1e8);
return satsToVoid(sats);
}

View File

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

108
lib/siteNav.ts Normal file
View File

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

View File

@@ -1,38 +1,114 @@
const USD_KEY = "cyberlux-usd-store-credit";
const TX_KEY = "cyberlux-btc-claimed-txids";
/**
* Per-account USD balance (from verified Bitcoin deposits), LUX loyalty points,
* and claimed txids — all keyed by CyberLux handle (same origin = one account).
*/
export function loadUsdStoreCredit(): number {
if (typeof window === "undefined") return 0;
export type AccountLedgerRow = {
usd: number;
lux: number;
claimedTxids: string[];
};
const LEDGER_KEY = "cyberlux-account-ledger-v1";
/** Preper-account migration keys (device-wide). */
const LEGACY_USD_KEY = "cyberlux-usd-store-credit";
const LEGACY_TX_KEY = "cyberlux-btc-claimed-txids";
function emptyRow(): AccountLedgerRow {
return { usd: 0, lux: 0, claimedTxids: [] };
}
function loadLedgerMap(): Record<string, AccountLedgerRow> {
if (typeof window === "undefined") return {};
try {
const raw = localStorage.getItem(USD_KEY);
if (!raw) return 0;
const raw = localStorage.getItem(LEDGER_KEY);
if (!raw) return {};
const o = JSON.parse(raw) as Record<string, unknown>;
if (!o || typeof o !== "object") return {};
const out: Record<string, AccountLedgerRow> = {};
for (const [k, v] of Object.entries(o)) {
if (!k || typeof v !== "object" || v === null) continue;
const row = v as Record<string, unknown>;
const usd = typeof row.usd === "number" && Number.isFinite(row.usd) ? Math.max(0, Math.round(row.usd * 100) / 100) : 0;
const lux = typeof row.lux === "number" && Number.isFinite(row.lux) ? Math.max(0, Math.floor(row.lux)) : 0;
const claimedTxids = Array.isArray(row.claimedTxids)
? row.claimedTxids.map((t) => String(t)).filter((t) => /^[a-fA-F0-9]{64}$/.test(t))
: [];
out[k.toLowerCase()] = { usd, lux, claimedTxids: [...new Set(claimedTxids)] };
}
return out;
} catch {
return {};
}
}
function saveLedgerMap(m: Record<string, AccountLedgerRow>): void {
if (typeof window === "undefined") return;
localStorage.setItem(LEDGER_KEY, JSON.stringify(m));
}
function accountKey(username: string): string {
return username.trim().toLowerCase();
}
export function getLedgerRow(username: string | null): AccountLedgerRow {
if (!username) return emptyRow();
const map = loadLedgerMap();
return map[accountKey(username)] ?? emptyRow();
}
export function setLedgerRow(username: string, row: AccountLedgerRow): void {
const map = loadLedgerMap();
map[accountKey(username)] = {
usd: Math.max(0, Math.round(row.usd * 100) / 100),
lux: Math.max(0, Math.floor(row.lux)),
claimedTxids: [...new Set(row.claimedTxids.filter((t) => /^[a-fA-F0-9]{64}$/.test(t)))],
};
saveLedgerMap(map);
}
/**
* One-time: move old device-wide USD + txids into this handles ledger, then clear legacy keys.
*/
export function migrateLegacyDeviceLedgerIfNeeded(username: string): void {
if (typeof window === "undefined" || !username.trim()) return;
const key = accountKey(username);
const map = loadLedgerMap();
const existing = map[key] ?? emptyRow();
if (existing.usd > 0 || existing.claimedTxids.length > 0) return;
let legacyUsd = 0;
try {
const raw = localStorage.getItem(LEGACY_USD_KEY);
if (raw) {
const n = Number.parseFloat(raw);
return Number.isFinite(n) ? Math.round(n * 100) / 100 : 0;
if (Number.isFinite(n)) legacyUsd = Math.max(0, Math.round(n * 100) / 100);
}
} catch {
return 0;
}
/* ignore */
}
export function saveUsdStoreCredit(usd: number): void {
if (typeof window === "undefined") return;
const safe = Math.max(0, Math.round(usd * 100) / 100);
localStorage.setItem(USD_KEY, String(safe));
}
export function loadClaimedTxids(): string[] {
if (typeof window === "undefined") return [];
let legacyTx: string[] = [];
try {
const raw = localStorage.getItem(TX_KEY);
if (!raw) return [];
const raw = localStorage.getItem(LEGACY_TX_KEY);
if (raw) {
const parsed = JSON.parse(raw) as unknown;
if (!Array.isArray(parsed)) return [];
return parsed.map((x) => String(x)).filter((t) => /^[a-fA-F0-9]{64}$/.test(t));
} catch {
return [];
if (Array.isArray(parsed)) {
legacyTx = parsed.map((t) => String(t)).filter((t) => /^[a-fA-F0-9]{64}$/.test(t));
}
}
} catch {
/* ignore */
}
export function saveClaimedTxids(txids: string[]): void {
if (typeof window === "undefined") return;
localStorage.setItem(TX_KEY, JSON.stringify([...new Set(txids)]));
if (legacyUsd <= 0 && legacyTx.length === 0) return;
map[key] = {
usd: legacyUsd,
lux: 0,
claimedTxids: [...new Set(legacyTx)],
};
saveLedgerMap(map);
localStorage.removeItem(LEGACY_USD_KEY);
localStorage.removeItem(LEGACY_TX_KEY);
}

136
lib/toolsCatalog.ts Normal file
View File

@@ -0,0 +1,136 @@
/**
* CyberLux tools catalog.
* Tools are unlocked permanently per handle by spending VOID credits server-side.
*/
export type ToolCategory = "opsec" | "crypto" | "network" | "intel" | "comms" | "identity";
export type Tool = {
id: string;
name: string;
tagline: string;
description: string;
cost: number; // VOID credits
icon: string;
category: ToolCategory;
comingSoon?: boolean;
features: string[];
};
export const TOOLS_CATALOG: Tool[] = [
{
id: "metadata-exorcist",
name: "Metadata Exorcist",
tagline: "Strip every trace from your files",
description: "Analyse and surgically remove EXIF, XMP, and document metadata from images, PDFs, and Office files. Paste base64 or upload directly.",
cost: 5,
icon: "🧹",
category: "opsec",
features: ["EXIF / XMP removal", "PDF metadata wipe", "Batch processing", "Before/after diff view"],
},
{
id: "pgp-forge",
name: "PGP Forge",
tagline: "Generate, sign, encrypt — all in-browser",
description: "Full PGP key management suite: generate Ed25519 or RSA-4096 keypairs, sign messages, verify signatures, and encrypt / decrypt payloads.",
cost: 6,
icon: "🔐",
category: "crypto",
features: ["Ed25519 & RSA-4096", "Sign / verify", "Encrypt / decrypt", "Key import & export", "Keyring manager"],
},
{
id: "shadow-trace",
name: "Shadow Trace",
tagline: "Analyse your OPSEC exposure score",
description: "Enter a handle, domain, or BTC address. Shadow Trace correlates public darknet indexing to give you an operational security score and surface visible leaks.",
cost: 8,
icon: "👁",
category: "opsec",
features: ["Handle exposure scan", "Domain footprint", "BTC address graph", "OPSEC score + remediation"],
},
{
id: "vpn-leak-tester",
name: "Leak Oracle",
tagline: "Verify what your circuit actually exposes",
description: "Multi-vector leak tester: DNS, WebRTC, IPv6, HTTP headers, and TLS fingerprint — run inside Tor Browser to verify your circuit leaks nothing.",
cost: 10,
icon: "🕳️",
category: "network",
features: ["DNS leak check", "WebRTC detection", "IPv6 probe", "Header fingerprint", "TLS JA3 hash"],
},
{
id: "tor-circuit-inspector",
name: "Circuit Inspector",
tagline: "Visualise and audit your Tor path",
description: "Real-time Tor circuit visualisation with relay details, exit geolocation, bandwidth stats, and circuit switching controls.",
cost: 12,
icon: "🌐",
category: "network",
features: ["Live circuit map", "Relay GeoIP lookup", "Exit node audit", "Circuit rebuild trigger", "Guard node history"],
},
{
id: "breach-oracle",
name: "Breach Oracle",
tagline: "Check credentials against known dumps",
description: "Query anonymised breach indices for email addresses, usernames, and password hashes (SHA-1 k-anonymity prefix). Nothing is logged.",
cost: 15,
icon: "🗄️",
category: "intel",
features: ["Email / handle check", "HIBP-compatible API", "Password hash prefix probe", "Zero-log architecture", "Bulk CSV mode"],
},
{
id: "deep-crawler",
name: "Void Crawler Pro",
tagline: "Deep darknet site discovery engine",
description: "Expanded crawl index with link graph, freshness scores, and category clustering. Goes beyond what the public Void Crawl surface exposes.",
cost: 20,
icon: "🕷️",
category: "intel",
features: ["Extended onion index", "Link graph visualiser", "Freshness tracker", "Category filter", "Export JSON / CSV"],
},
{
id: "signal-ghost",
name: "Signal Ghost",
tagline: "Ephemeral encrypted channel generator",
description: "Create a one-shot end-to-end encrypted channel. Share the link; both parties exchange messages; channel self-destructs after timeout or after close.",
cost: 25,
icon: "👻",
category: "comms",
features: ["X25519 + AES-GCM in-browser", "Zero-server-storage", "Configurable TTL", "Screenshot-proof mode", "File attachment (≤ 5 MB)"],
},
{
id: "onion-architect",
name: "Onion Architect",
tagline: "Guided hidden service provisioning wizard",
description: "Step-by-step wizard for setting up Tor v3 hidden services: generates torrc stubs, nginx configs, systemd units, and security hardening checklists.",
cost: 35,
icon: "🧅",
category: "network",
features: ["torrc generator", "nginx vhost builder", "systemd unit wizard", "UFW hardening script", "Vanity prefix guide"],
comingSoon: false,
},
{
id: "identity-auditor",
name: "Identity Auditor",
tagline: "Full digital identity surface review",
description: "Comprehensive review of a handle or identity across known darknet forums, paste sites, and public indices. Produces a redacted report with a risk rating.",
cost: 40,
icon: "🪪",
category: "identity",
features: ["Forum cross-reference", "Paste site scan", "Writing-style consistency flag", "Risk score", "Downloadable report"],
comingSoon: true,
},
];
export function getToolById(id: string): Tool | undefined {
return TOOLS_CATALOG.find((t) => t.id === id);
}
export const CATEGORY_LABELS: Record<ToolCategory, string> = {
opsec: "OPSEC",
crypto: "Crypto",
network: "Network",
intel: "Intelligence",
comms: "Comms",
identity: "Identity",
};

View File

@@ -1,10 +1,10 @@
# CyberLux — hub + generated entry onions (see nginx/cyberlux-onion-servers.inc).
# CyberLux — single onion ingress.
# Request handling: lib/onionRoutes.generated.ts + proxy.ts (Next.js 16+)
#
# Hub: 127.0.0.1:8080 — no X-Cyberlux-Node (full app)
# Other loopback ports: see scripts/onion-nodes.json → scripts/generate-onion-config.cjs
# Hub: 127.0.0.1:8080 — full app, all sections as paths
#
# Requires /etc/nginx/cyberlux-server-common.inc and /etc/nginx/cyberlux-onion-servers.inc
# Requires /etc/nginx/conf.d/cyberlux-ddos-zones.conf
# and /etc/nginx/cyberlux-server-common.inc
# (installed by scripts/install-tor-onion.sh).
# --- HUB (main storefront) ---
@@ -17,7 +17,28 @@ server {
client_max_body_size 25m;
# ── Next.js static assets — long cache, NO rate limiting ──────────────────
# These are content-hashed immutable files. Rate-limiting them causes the
# Tor Browser to silently drop CSS/JS on first load (burst exhausted by the
# HTML request before assets arrive).
location /_next/static/ {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host 127.0.0.1;
proxy_set_header X-Forwarded-Proto http;
add_header Cache-Control "public, max-age=31536000, immutable" always;
access_log off;
}
# ── App routes — rate limited ─────────────────────────────────────────────
location / {
# Tor forwards every onion client from loopback, so zones are keyed
# by $server_port to avoid collapsing all visitors into one bucket.
limit_req zone=cyberlux_onion_req burst=120 nodelay;
limit_conn cyberlux_onion_conn 60;
limit_req_status 429;
limit_conn_status 429;
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
@@ -30,5 +51,3 @@ server {
proxy_read_timeout 86400;
}
}
include /etc/nginx/cyberlux-onion-servers.inc;

View File

@@ -0,0 +1,12 @@
#
# CyberLux onion ingress budgets.
# Installed into /etc/nginx/conf.d/ so these zones exist in nginx `http { }`.
#
# Tor hidden services connect to nginx over 127.0.0.1, so per-IP limits are not
# useful here. `$server_port` keys the CyberLux onion ingress bucket.
#
# Keyed by server_port — not remote_addr, because all Tor clients arrive from loopback.
# rate=60r/s is generous for onion traffic; static assets are excluded from this zone
# (see nginx.example.conf /_next/static/ location block).
limit_req_zone $server_port zone=cyberlux_onion_req:10m rate=60r/s;
limit_conn_zone $server_port zone=cyberlux_onion_conn:10m;

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More