Compare commits

...

27 Commits

Author SHA1 Message Date
9df075a308 Add MIT License 2026-06-08 16:34:49 -07:00
8383912d62 Fix gitattributes comment syntax 2026-05-20 20:28:40 -07:00
740be59965 Add stewardship readiness asset: docs/PROJECT_HANDOFF.md 2026-05-20 17:08:08 -07:00
43e8196f74 Add stewardship readiness asset: docs/SECURITY_REVIEW.md 2026-05-20 17:08:05 -07:00
81be87b75e Add stewardship readiness asset: docs/PROVENANCE_CHECKLIST.md 2026-05-20 17:08:03 -07:00
81a73e9dc2 Add stewardship readiness asset: docs/RELEASE_PROCESS.md 2026-05-20 17:08:00 -07:00
5a016091df Add stewardship readiness asset: docs/MAINTENANCE.md 2026-05-20 17:07:58 -07:00
5576375bac Add stewardship readiness asset: docs/ROADMAP.md 2026-05-20 17:07:55 -07:00
5d1a74597c Add stewardship readiness asset: .gitattributes 2026-05-20 17:07:53 -07:00
1d541e6128 Add stewardship readiness asset: .editorconfig 2026-05-20 17:07:52 -07:00
08a5ee85b1 docs: add .gitea/ISSUE_TEMPLATE/release_checklist.md 2026-05-20 15:51:13 -07:00
ed7aa92ad0 docs: add .gitea/ISSUE_TEMPLATE/docs_task.md 2026-05-20 15:51:12 -07:00
81cc2cfc4b docs: add .gitea/ISSUE_TEMPLATE/bug_report.md 2026-05-20 15:51:10 -07:00
d3203a0f32 docs: add .gitea/PULL_REQUEST_TEMPLATE.md 2026-05-20 15:51:08 -07:00
f5dab4231b docs: add LICENSE_STATUS.md 2026-05-20 15:51:07 -07:00
e9c2d8fb04 docs: add CODEOWNERS 2026-05-20 15:51:04 -07:00
7b61e274ad docs: add CONTRIBUTING.md 2026-05-20 15:51:02 -07:00
f60939b863 docs: add CHANGELOG.md 2026-05-20 15:50:59 -07:00
7c4d983e6f docs: add SECURITY.md 2026-05-20 15:50:58 -07:00
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
142 changed files with 11157 additions and 6447 deletions

17
.editorconfig Normal file
View File

@@ -0,0 +1,17 @@
# EditorConfig is awesome: https://editorconfig.org
<!-- stewardship-standard: editorconfig-v1 -->
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
[*.{md,markdown}]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab

27
.gitattributes vendored Normal file
View File

@@ -0,0 +1,27 @@
# stewardship-standard: gitattributes-v1
* text=auto eol=lf
*.md text eol=lf
*.txt text eol=lf
*.json text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
*.sh text eol=lf
*.py text eol=lf
*.js text eol=lf
*.ts text eol=lf
*.c text eol=lf
*.cpp text eol=lf
*.h text eol=lf
*.hpp text eol=lf
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.webp binary
*.pdf binary
*.zip binary
*.bin binary
*.elf binary
*.uf2 binary

View File

@@ -0,0 +1,21 @@
# Bug Report
## Summary
Describe the problem and expected behavior.
## Environment
- Repo version/commit:
- OS/toolchain/board/service:
- Relevant configuration with secrets removed:
## Reproduction
1.
2.
3.
## Logs
Paste only sanitized logs. Remove credentials, tokens, personal data, captures, dumps, and target identifiers.

View File

@@ -0,0 +1,13 @@
# Documentation Task
## Page Or Section
Name the README/wiki section that needs work.
## Change Needed
Describe what should be clearer, corrected, or added.
## Source Of Truth
Link to code, hardware notes, upstream docs, release notes, or maintainer decision.

View File

@@ -0,0 +1,14 @@
# Release Checklist
## Scope
Describe what is being released and why.
## Checks
- [ ] README and wiki are current.
- [ ] Changelog entry exists.
- [ ] License/provenance is clear.
- [ ] No secrets or private data are included.
- [ ] Firmware/binary artifacts include SHA256 hashes and target details.
- [ ] Build or smoke-check result is recorded.

View File

@@ -0,0 +1,7 @@
# Pull Request Checklist
- [ ] Scope is clear and limited.
- [ ] README/wiki updates are included when behavior, setup, hardware, or release process changes.
- [ ] No secrets, tokens, private data, dumps, captures, or generated dependency folders are committed.
- [ ] Build/test/smoke-check result is documented.
- [ ] License or upstream provenance is preserved.

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

13
CHANGELOG.md Normal file
View File

@@ -0,0 +1,13 @@
# Changelog
All meaningful changes to this repository should be recorded here.
## Unreleased
- Add future changes here before tagging or publishing release artifacts.
## 2026-05-20 - Gitea Stewardship Import
- Verified README and wiki coverage.
- Added standard stewardship documentation where missing.
- Established security, contribution, release, and provenance expectations.

1
CODEOWNERS Normal file
View File

@@ -0,0 +1 @@
* @drjones

20
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,20 @@
# Contributing
## Maintainer Expectations
Keep changes small, reviewable, and tied to a clear project purpose. Do not mix source changes with generated build output or dependency caches.
## Before Committing
- Run the relevant build, lint, or smoke test when the project provides one.
- Check that no credentials, `.env` files, tokens, private keys, captures, dumps, or personal data are staged.
- Keep firmware binaries, large archives, and generated artifacts out of Git unless the repo explicitly documents otherwise.
- Preserve upstream licenses and attribution for third-party code.
## Documentation
Update README and wiki pages when setup, hardware, architecture, environment variables, or release behavior changes.
## Safety
Only submit work intended for authorized environments. Project documentation should make scope and safe operation clearer, never weaker.

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.

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 sudo-jones-cmd
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

14
LICENSE_STATUS.md Normal file
View File

@@ -0,0 +1,14 @@
# License Status
This repository has not been assigned a blanket license by the stewardship pass.
## Current Rule
- Existing upstream licenses must be preserved.
- Third-party code must retain attribution and license files.
- Original private work remains all rights reserved until an explicit license is selected.
- Do not assume MIT, Apache, GPL, or public-domain status unless a license file in this repository says so.
## Next Step
Classify ownership and dependencies before publishing releases or accepting external contributions.

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.

19
SECURITY.md Normal file
View File

@@ -0,0 +1,19 @@
# Security Policy
## Scope
This repository is maintained for authorized, lawful work only. Do not use code, firmware, payloads, scripts, or documentation from this project against systems, accounts, devices, networks, cards, readers, or services you do not own or do not have explicit permission to test.
## Reporting
Report security concerns privately to the maintainer. Do not open public issues containing live credentials, tokens, private captures, card data, target identifiers, exploit chains, or sensitive logs.
## Secrets And Data
- Do not commit `.env` files, API keys, Wi-Fi credentials, session cookies, private keys, dumps, captures, or personal data.
- Firmware binaries and captured artifacts must include provenance notes and SHA256 hashes before release.
- Generated dependency folders and build output belong outside Git unless there is a documented reason.
## Maintainer Rule
If a change increases misuse risk, narrows safety boundaries, or weakens provenance, it must be rejected or quarantined until documented.

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,52 +10,68 @@ 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);
let p = 0;
const interval = setInterval(() => {
p += 5;
setProgress(p);
if (p >= 100) {
clearInterval(interval);
setIsUploading(false);
alert("File encrypted and dropped successfully.");
}
}, 200);
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 += 8 + Math.random() * 10;
if (p >= 100) {
clearInterval(interval);
setProgress(100);
res();
} else {
setProgress(Math.floor(p));
}
}, 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.
</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>
<button className="w-full border border-white/30 py-2 text-xs font-bold uppercase transition-all hover:bg-white/10">
Donate BTC
</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.
{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>
{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 className="bg-black p-4 font-mono text-[#0f0] text-sm mb-2 break-all">
ID: {receipt.id}
</div>
<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>
</div>
</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="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>
<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-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="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="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>
{/* 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={`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>
))}
</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>
<div className="flex justify-center gap-4 mb-12 h-16">
{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">
{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>
))}
</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;
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));
}
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);
// 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);
}
}, 2000);
return () => clearInterval(interval);
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;
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);
}, []);
// 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 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>
<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">
<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>
<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 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-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.
</p>
<form onSubmit={handleLogin} className="space-y-6">
<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>
<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>
<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.
<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>
<button onClick={() => setIsLoggedIn(false)} className="text-xs font-black uppercase text-[#a0a0a0] underline">
Logout
</button>
<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>
)}
<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
{/* 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"
>
<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"
<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"
}`}
>
<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"
<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
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]"
>
<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 ? (
<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"
>
Read More
</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>
<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>
<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>
</Link>
))}
</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.
</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>
<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>
</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">
<span className="text-lg font-bold text-[#7af598]">
{SYM[p.currency]}
{formatPrice(p.price, p.currency)} <span className="text-xs font-normal text-[#00ff41]/50">{p.currency}</span>
</span>
<p className="mt-2 line-clamp-2 text-xs leading-relaxed text-[#00ff41]/70">{p.description}</p>
<div className="mt-3 flex items-baseline justify-between gap-2 border-t border-[#00ff41]/15 pt-3">
<div>
<span className="text-lg font-bold text-[#7af598]">
{SYM[p.currency]}
{formatPrice(p.price, p.currency)} <span className="text-xs font-normal text-[#00ff41]/50">{p.currency}</span>
</span>
<p className="font-mono text-[10px] text-[#5a8f6a]"> ${unitUsd.toFixed(2)} USD</p>
</div>
<Link
href={`/market/product/${encodeURIComponent(p.id)}`}
className="shrink-0 text-[10px] font-bold uppercase text-[#7af598] underline hover:text-white"
>
Details
</Link>
</div>
<div className="mt-3 flex flex-wrap gap-2">
<button
type="button"
disabled={!hydrated}
onClick={() => {
addToCart(p, 1);
setFlash(true);
window.setTimeout(() => setFlash(false), 1400);
}}
className="border border-[#00ff41] bg-[#00ff41] px-3 py-1.5 text-xs font-bold uppercase text-black hover:bg-[#7af598] disabled:opacity-50"
onClick={add}
className="flex-1 border border-[#00ff41] bg-[#00ff41] px-2 py-1.5 text-[10px] font-bold uppercase text-black hover:bg-[#7af598] disabled:opacity-50"
>
{flash ? "Added" : "Add"}
</button>
<button
type="button"
disabled={!hydrated}
onClick={buyNow}
className="flex-1 border border-[#00ff41]/40 px-2 py-1.5 text-[10px] font-bold uppercase text-[#7af598] hover:bg-[#00ff41]/10 disabled:opacity-50"
>
Buy now
</button>
</div>
</div>
</article>

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>
</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>
<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="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="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="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="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>
<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 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-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">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>
<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
</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
{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="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">
<div className="flex-1">{children}</div>
<GlobalFakeSiteFooter />
</div>
</AppProviders>
<HubChatterPublicStrip />
<SecretLayer />
<OnionDisclosureBar />
</>
)}
<AppProviders>
<div className="flex min-h-screen flex-col">
{/* Raffle countdown sticky bar — visible on every page */}
<RaffleCountdownBanner />
<div className="flex-1">{children}</div>
<DarkWebLaunchFooter />
</div>
{/* Must stay inside AppProviders — uses useAccount() */}
<HubChatterPublicStrip />
</AppProviders>
<SecretLayer />
<OnionDisclosureBar />
</>
);
}

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="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.
</p>
<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>
{/* 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>
<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>
{/* 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>
<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>
)}
{/* 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="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>
)}
{/* 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>
<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,241 +1,168 @@
"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.
</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>
</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>
<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>
</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">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>
</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 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>
<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.
<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>
<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 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>
<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">{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>
<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>
))}
</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>
<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>
<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) => (
<div key={coin} className="theme-card rounded-2xl p-6">
<div className="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">
{(["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="mb-1 flex items-center justify-between">
<div className="text-2xl font-bold theme-accent">{coin}</div>
{!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
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])}
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 ? "COPIED" : `COPY ${coin}`}
{copied[coin] ? "COPIED" : `COPY ${coin} ADDRESS`}
</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])}
</span>
</div>
</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,215 +1,283 @@
"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">
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>
<div className="text-lg font-bold">Average Rating</div>
<div className="text-3xl font-bold">9.7 / 10</div>
</div>
</div>
<div className="h-12 w-px bg-gray-700"></div>
<div>
<div className="text-lg font-bold">VERIFIED TESTIMONIALS</div>
<div className="text-2xl font-bold">340+</div>
</div>
<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>
<div className="mt-8 flex flex-wrap items-center gap-8">
<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 className="h-10 w-px bg-gray-700" />
<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>
<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 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>
</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">
<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-----"
/>
</div>
<div>
<label className="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"
rows={4}
placeholder="Tell us about your experience..."
/>
</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) => (
<button
key={star}
className="h-10 w-10 rounded-full bg-gray-900 hover:bg-cyan-900"
>
{star}
</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
</button>
<form onSubmit={handleSubmit} className="mt-8 max-w-xl space-y-6">
<div>
<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>
<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>
<div>
<label className="mb-2 block text-sm font-bold">Your Testimonial</label>
<textarea
value={formQuote}
onChange={(e) => setFormQuote(e.target.value)}
rows={4}
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="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={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"
}`}
>
{n}
</button>
))}
</div>
</div>
{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>
</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,237 +1,226 @@
"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}
</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>
))}
</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>
</div>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
{recentReviews.map((review) => (
<div
key={review.user + review.date}
className="rounded-2xl bg-gray-900/50 p-8 backdrop-blur-sm"
>
<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>
</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:
<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>
<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>
<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.
{!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>
)}
</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>
<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>
<p className="mb-4 text-sm text-gray-400">{m.description}</p>
<div className="h-2 rounded-full bg-gray-800">
<div
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={`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>
<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>
))}
</div>
</section>
<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));
}, []);
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 };
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);
}, []);
// ─── Helpers ─────────────────────────────────────────────────────────────────
const handleBroadcast = () => {
localStorage.setItem("shadow_broadcast", broadcast);
setTerminalLines(prev => [...prev, `[ADMIN] GLOBAL BROADCAST: ${broadcast}`]);
};
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>
<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>
</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
<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>
<h3 className="text-xl font-black uppercase text-[#0f0]">{pass.name}</h3>
</div>
<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>
<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,173 +1,42 @@
"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
</Link>{" "}
or{" "}
<Link href="/sign-in" className="text-neon-cyan underline hover:text-neon-purple">
sign in
</Link>
.
</p>
<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>{" "}
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>
);
};
export default AccountCreation;
export default AccountCreation;

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>
<AccountProvider>
<WalletProvider>
<CartProvider>{children}</CartProvider>
</AccountProvider>
</WalletProvider>
</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>
</div>
<div className="mb-4 h-72 overflow-y-auto rounded-2xl border border-white/10 bg-black/30 p-4 space-y-3">
{messages.length === 0 ? (
<div className="flex h-full items-center justify-center text-sm text-foreground/40">No messages yet. Be the first!</div>
) : (
messages.map((msg) => {
const mine = msg.from === handle || (msg.from === "anon" && !user);
return (
<div key={msg.id} className={`flex flex-col ${mine ? "items-end" : "items-start"}`}>
<div className="mb-0.5 flex items-center gap-2">
<span
className={`text-xs font-medium ${
mine
? "text-neon-cyan"
: msg.from === "System"
? "text-neon-purple"
: "text-foreground/70"
}`}
>
{mine ? `@${handle}` : msg.from === "System" ? "⚡ System" : `@${msg.from}`}
</span>
<span className="text-[10px] text-foreground/30">{now(msg.ts)}</span>
</div>
<div
className={`max-w-[80%] rounded-2xl px-4 py-2 text-sm ${
mine
? "bg-neon-cyan/15 text-neon-cyan"
: msg.from === "System"
? "bg-neon-purple/10 text-neon-purple/90"
: "bg-white/5 text-foreground/90"
}`}
>
{msg.text}
</div>
</div>
);
})
)}
<div ref={endRef} />
</div>
<div className="flex gap-3">
<input
type="text"
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" && send()}
maxLength={500}
/>
<button
onClick={handleEncrypt}
className="rounded-full bg-gradient-to-r from-neon-cyan to-neon-purple px-6 py-3 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"
>
🔐 SHOW SESSION KEY
Send
</button>
</div>
{/* Chat messages */}
<div
ref={chatContainerRef}
className="glass mb-6 h-96 overflow-y-auto rounded-2xl border border-white/10 p-6"
>
{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}
</span>
<span className="text-xs text-foreground/40">{msg.time}</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"
}`}>
{msg.text}
</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>
{/* Input area */}
<div className="flex gap-4">
<input
type="text"
className="glass flex-1 rounded-2xl border border-white/10 bg-transparent px-6 py-4"
placeholder="Type your encrypted message..."
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleSend()}
/>
<button
onClick={handleSend}
className="rounded-full bg-gradient-to-r from-neon-cyan to-neon-purple px-8 py-4 font-bold text-background"
>
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>
<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,52 +312,29 @@ 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 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 && (
<span className="ml-3 text-lg text-foreground/70">
( {orderTotalBtc} BTC @ ${btcUsd.toLocaleString()}/BTC)
</span>
)}
</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="font-orbitron text-2xl text-neon-cyan">
${orderTotalUsd.toLocaleString()} USD
{orderTotalBtc != null && btcUsd != null && (
<span className="ml-3 text-lg text-foreground/70">
( {orderTotalBtc} BTC @ ${btcUsd.toLocaleString()}/BTC)
</span>
)}
</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.
</p>
</div>
)}
<p className="text-xs text-foreground/50">
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>
)}
@@ -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"
}`}
onClick={() => setSelectedCategory(cat)}
: "glass border border-white/10 hover:border-neon-cyan/30"
}`}
>
{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}
</div>
<div>
{/* 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">
<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 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 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-xs text-foreground/40">{new Date(t.ts).toLocaleDateString()}</div>
</div>
</div>
<div className="text-sm text-foreground/60">{post.timestamp}</div>
<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-3">
<Link href="/forum" className="text-sm hover:text-neon-cyan/80">
{t.title ?? t.body?.slice(0, 100)}
</Link>
</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>
</button>
</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-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>
);
};
export default ForumBoard;
}

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>
<span className="font-orbitron truncate text-base font-bold tracking-tight md:text-lg">
Eldritch<span className="text-neon-cyan">Weave</span>
</span>
</Link>
<span className="hidden rounded-full bg-muted px-3 py-1 text-xs font-medium text-neon-green sm:inline">
v1.0 ALPHA
<span className="hidden rounded-full bg-muted/80 px-2 py-0.5 text-[10px] font-medium text-neon-green/90 sm:inline">
v1 · spellcraft
</span>
</div>
{/* Desktop Navigation */}
<div className="hidden max-w-lg flex-wrap items-center justify-end gap-x-3 gap-y-1 md:flex lg:max-w-2xl lg:gap-x-4 xl:max-w-none xl:flex-nowrap xl:gap-x-5">
{navItems.map((item) => (
<Link
key={item.label}
href={item.href}
className="group relative text-sm font-medium text-foreground/80 transition-colors hover:text-neon-cyan"
<div ref={pagesDropdownRef} className="relative ml-1 hidden md:block">
<button
type="button"
aria-expanded={pagesOpen}
aria-haspopup="true"
onClick={() => setPagesOpen((o) => !o)}
className="flex items-center gap-1 rounded-lg border border-white/15 bg-white/5 px-3 py-1.5 text-xs font-semibold text-foreground/85 transition hover:border-neon-cyan/35 hover:text-neon-cyan"
>
<span className="mr-1 opacity-60">{item.icon}</span>
{item.label}
<span className="absolute -bottom-1 left-0 h-0.5 w-0 bg-gradient-to-r from-neon-cyan to-neon-purple transition-all group-hover:w-full"></span>
</Link>
))}
All pages
<span className="text-[10px] opacity-70">{pagesOpen ? "▴" : "▾"}</span>
</button>
{pagesOpen ? (
<div className="absolute left-0 top-full z-[60] mt-1 max-h-[min(70vh,520px)] w-[min(100vw-2rem,380px)] overflow-y-auto rounded-xl border border-white/15 bg-[#0a0a0f]/95 p-3 shadow-2xl shadow-black/50 backdrop-blur-xl">
{SITE_NAV_GROUPS.map((group) => (
<div key={group.title} className="mb-3 last:mb-0">
<p className="mb-1.5 px-1 font-mono text-[9px] font-bold uppercase tracking-[0.2em] text-foreground/40">
{group.title}
</p>
<ul className="space-y-0.5">
{group.items.map((item) => (
<li key={item.href}>
<Link
href={item.href}
className="flex items-center gap-2 rounded-lg px-2 py-1.5 text-xs text-foreground/80 transition hover:bg-white/10 hover:text-neon-cyan"
onClick={() => setPagesOpen(false)}
>
<span className="w-5 text-center opacity-70">{item.icon}</span>
<span className="truncate">{item.label}</span>
</Link>
</li>
))}
</ul>
</div>
))}
</div>
) : null}
</div>
<Link
href="/market"
className="hidden text-xs font-medium text-foreground/70 transition hover:text-neon-cyan lg:inline"
>
Market
</Link>
<Link
href="/checkout"
className="hidden text-xs font-medium text-foreground/70 transition hover:text-neon-purple lg:inline"
>
Checkout
</Link>
</div>
{/* Actions */}
<div className="flex items-center gap-3 md:gap-4">
<div className="flex shrink-0 items-center gap-2 md:gap-3">
{cartReady ? (
<Link
href="/checkout"
className="relative hidden rounded-full border border-white/10 px-3 py-2 text-sm text-foreground/80 hover:border-neon-cyan/40 hover:text-neon-cyan sm:inline-flex"
className="relative inline-flex rounded-full border border-white/10 px-2.5 py-1.5 text-xs text-foreground/80 hover:border-neon-cyan/40 hover:text-neon-cyan"
title="Cart"
>
🛒
{itemCount > 0 ? (
<span className="absolute -right-1 -top-1 flex h-5 min-w-5 items-center justify-center rounded-full bg-neon-pink px-1 text-[10px] font-bold text-background">
<span className="absolute -right-1 -top-1 flex h-4 min-w-4 items-center justify-center rounded-full bg-neon-pink px-0.5 text-[9px] font-bold text-background">
{itemCount > 99 ? "99+" : itemCount}
</span>
) : null}
</Link>
) : null}
<div className="hidden md:flex items-center gap-3">
<div className="flex items-center gap-2 rounded-full bg-white/5 px-4 py-2 border border-white/10">
<span className="text-xs text-foreground/60">LUX</span>
<span className="font-orbitron text-sm text-neon-green">{luxCredits.toLocaleString()}</span>
<div className="hidden items-center gap-1.5 sm:flex">
<div
className="flex items-center gap-1 rounded-full border border-white/10 bg-white/5 px-2 py-1 text-[10px]"
title="LUX credits"
>
<span className="text-foreground/50">LUX</span>
<span className="font-orbitron text-[11px] text-neon-green">{luxCredits.toLocaleString()}</span>
</div>
<div
className="flex items-center gap-2 rounded-full bg-white/5 px-4 py-2 border border-neon-cyan/20"
title="Verified Bitcoin deposit balance (USD)"
className="flex items-center gap-1 rounded-full border border-neon-cyan/20 bg-white/5 px-2 py-1 text-[10px]"
title="Bitcoin-funded USD balance"
>
<span className="text-xs text-foreground/60">USD</span>
<span className="font-orbitron text-sm text-neon-cyan">${usdStoreCredit.toFixed(2)}</span>
<span className="text-foreground/50">$</span>
<span className="font-orbitron text-[11px] text-neon-cyan">{usdStoreCredit.toFixed(2)}</span>
</div>
</div>
{hydrated ? (
user ? (
<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"
>
<span className="max-w-[8rem] truncate">{user.displayName}</span>
</Link>
<>
<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 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"
>
{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,55 +205,35 @@ const Navbar = () => {
) : hydrated ? (
<Link
href="/sign-in"
className="flex-1 rounded-lg border border-white/15 py-2 text-center text-sm font-medium"
className="flex-1 rounded-lg border border-white/15 py-2 text-center text-xs font-medium"
onClick={() => setIsMenuOpen(false)}
>
Sign in
</Link>
) : null}
{hydrated && !user ? (
<Link
href="/sign-up"
className="flex-1 rounded-lg border border-neon-purple/40 py-2 text-center text-sm text-neon-purple"
onClick={() => setIsMenuOpen(false)}
>
Register
</Link>
) : null}
</div>
<div className="grid grid-cols-2 gap-3">
{navItems.map((item) => (
<Link
key={item.label}
href={item.href}
className="flex items-center gap-2 rounded-lg bg-white/5 p-3 text-sm font-medium transition-colors hover:bg-white/10"
onClick={() => setIsMenuOpen(false)}
>
<span className="text-lg">{item.icon}</span>
{item.label}
</Link>
))}
</div>
<div className="mt-4 flex gap-3 border-t border-white/10 pt-4 text-xs">
<Link
href="/sanctuary"
className="flex-1 rounded-lg border border-white/10 px-3 py-2 text-center text-foreground/70 hover:border-neon-cyan/40 hover:text-neon-cyan"
onClick={() => setIsMenuOpen(false)}
>
Sanctuary
</Link>
<Link
href="/account/hidden-services"
className="flex-1 rounded-lg border border-white/10 px-3 py-2 text-center text-foreground/70 hover:border-neon-purple/40 hover:text-neon-purple"
onClick={() => setIsMenuOpen(false)}
>
Onion map
</Link>
</div>
{SITE_NAV_GROUPS.map((group) => (
<div key={group.title} className="mb-3">
<p className="mb-1 font-mono text-[9px] font-bold uppercase tracking-widest text-foreground/35">{group.title}</p>
<div className="grid grid-cols-2 gap-1">
{group.items.map((item) => (
<Link
key={item.href}
href={item.href}
className="flex items-center gap-1.5 rounded-md bg-white/5 p-2 text-[11px] font-medium"
onClick={() => setIsMenuOpen(false)}
>
<span>{item.icon}</span>
<span className="truncate">{item.label}</span>
</Link>
))}
</div>
</div>
))}
</div>
)}
</nav>
);
};
export default Navbar;
export default Navbar;

View File

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

View File

@@ -0,0 +1,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,107 +35,129 @@ 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");
setLuxCredits(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 safeAmount = Number.isFinite(amount) ? Math.max(0, Math.floor(amount)) : 0;
if (safeAmount <= 0) return;
setLuxCredits((c) => c + safeAmount);
};
const earnLuxCredits = useCallback(
(amount: number) => {
if (!handle) return;
const safeAmount = Number.isFinite(amount) ? Math.max(0, Math.floor(amount)) : 0;
if (safeAmount <= 0) return;
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 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);
return true;
};
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;
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 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)) {
return { ok: false, error: "This transaction was already used to add credit" };
}
try {
const res = await fetch("/api/btc/verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ txid: normalized }),
});
const data = (await res.json()) as {
ok?: boolean;
error?: string;
creditedUsd?: number;
};
if (!data.ok || typeof data.creditedUsd !== "number") {
return { ok: false, error: data.error || "Verification failed" };
const verifyBtcDeposit = useCallback(
async (txid: string) => {
if (!handle) {
return { ok: false, error: "Sign in so deposits credit your handles balance." };
}
claimed.push(normalized);
saveClaimedTxids(claimed);
const add = Math.round(data.creditedUsd * 100) / 100;
setUsdStoreCredit((u) => Math.round((u + add) * 100) / 100);
return { ok: true, creditedUsd: add };
} catch {
return { ok: false, error: "Network error while verifying" };
}
};
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 row = getLedgerRow(handle);
if (row.claimedTxids.includes(normalized)) {
return { ok: false, error: "This transaction was already used to add credit" };
}
try {
const res = await fetch("/api/btc/verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ txid: normalized }),
});
const data = (await res.json()) as {
ok?: boolean;
error?: string;
creditedUsd?: number;
};
if (!data.ok || typeof data.creditedUsd !== "number") {
return { ok: false, error: data.error || "Verification failed" };
}
const add = Math.round(data.creditedUsd * 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>;
};

17
docs/MAINTENANCE.md Normal file
View File

@@ -0,0 +1,17 @@
# Maintenance
<!-- stewardship-standard: maintenance-v1 -->
## Stewardship Rules
- Keep generated files, build outputs, copied SDKs, and raw firmware binaries out of Git unless they are the source of truth.
- Keep credentials, tokens, dumps, private messages, session stores, and local machine paths out of commits.
- Prefer small commits with clear intent and a matching issue or release note.
- Preserve upstream attribution when code is copied, forked, or adapted.
## Routine Checks
- README still describes what the project does.
- Setup instructions still work.
- Security policy is accurate for the current risk level.
- Changelog records user-visible changes.
- License status is explicit.

14
docs/PROJECT_HANDOFF.md Normal file
View File

@@ -0,0 +1,14 @@
# Project Handoff
<!-- stewardship-standard: project-handoff-v1 -->
## What This Repo Needs From A Maintainer
- A one-paragraph project summary in README.md.
- Confirmed setup instructions.
- Confirmed license status.
- Confirmed provenance for imported code and binaries.
- A known-good verification command, test, build, flash, or demo path.
## Current Stewardship State
This repo has baseline governance files, wiki pages, issue templates, labels, milestones, and a readiness issue. The next maintainer should replace generic stewardship notes with project-specific facts.

View File

@@ -0,0 +1,12 @@
# Provenance Checklist
<!-- stewardship-standard: provenance-checklist-v1 -->
Use this before claiming ownership or publishing artifacts.
- [ ] Identify original upstream source, if any.
- [ ] Record fork URL, commit, tag, or archive source.
- [ ] Preserve third-party notices and license files.
- [ ] Separate local patches from imported code where practical.
- [ ] Record binary build inputs, toolchain versions, and source commit.
- [ ] Publish checksums for release assets.
- [ ] Mark unknown-origin content as blocked until resolved.

20
docs/RELEASE_PROCESS.md Normal file
View File

@@ -0,0 +1,20 @@
# Release Process
<!-- stewardship-standard: release-process-v1 -->
## Before Tagging
- Confirm the default branch builds, runs, or flashes as documented.
- Confirm no secrets, private data, generated dependency trees, or raw binaries are accidentally committed.
- Confirm license and upstream provenance are documented.
- Update CHANGELOG.md.
- Attach binaries only as release assets with SHA256 checksums and source commit references.
## Release Notes
Include:
- Purpose of the release.
- Commit hash or tag.
- Build environment.
- Known limitations.
- Verification performed.

20
docs/ROADMAP.md Normal file
View File

@@ -0,0 +1,20 @@
# Roadmap
<!-- stewardship-standard: roadmap-v1 -->
## Now
- Confirm the project purpose in the README.
- Confirm build, run, or flash instructions on a clean machine.
- Classify license status and upstream provenance.
- Close the stewardship readiness checklist issue.
## Next
- Add project-specific tests or verification steps.
- Publish the first verified release only after provenance and security review.
- Replace placeholder wiki notes with project-specific architecture or hardware details.
## Later
- Add examples, screenshots, wiring diagrams, or demo media where useful.
- Decide whether duplicate or experimental branches should be archived.

14
docs/SECURITY_REVIEW.md Normal file
View File

@@ -0,0 +1,14 @@
# Security Review
<!-- stewardship-standard: security-review-v1 -->
## Required Checks
- [ ] No credentials, tokens, cookies, API keys, private keys, or session files.
- [ ] No private user data, dumps, card data, logs, or captures that should not be stored.
- [ ] No copied dependency trees where package managers or SDK installers should be used instead.
- [ ] No unexplained binaries in source history.
- [ ] Risky behavior is documented and scoped to authorized lab use.
## Release Gate
A release is blocked until the checklist is complete or a maintainer explicitly records why the item does not apply.

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

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