From 04d64fb99302b7bceb7323ba42ab9b9ef4b92048 Mon Sep 17 00:00:00 2001 From: drjones Date: Fri, 24 Apr 2026 23:23:48 -0700 Subject: [PATCH] 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 --- .gitignore | 4 + DEPLOY.md | 28 ++ app/account/page.tsx | 59 ++++ app/api/messages/route.ts | 79 +++++ app/globals.css | 40 +-- app/hidden-wiki/page.tsx | 8 +- app/layout.tsx | 6 +- app/links/page.tsx | 16 +- app/market/layout.tsx | 13 +- app/market/page.tsx | 178 +++++++++--- app/market/product/[id]/page.tsx | 220 ++++++++++++++ app/messages/page.tsx | 149 +++------- app/page.tsx | 30 +- app/raffle/page.tsx | 60 +++- app/root-layout-client.tsx | 46 ++- app/vault/page.tsx | 264 +++++++++++------ app/wallets/page.tsx | 12 + components/ChatWidget.tsx | 173 +++++------ components/DDoSProtection.tsx | 86 ++++-- components/Hero.tsx | 38 +-- components/Navbar.tsx | 246 ++++++++-------- components/PayWithUsdBalanceButton.tsx | 111 ++++++++ components/market/MarketCommerceBar.tsx | 46 +++ components/market/MarketSiteChrome.tsx | 10 +- lib/cyberluxAccount.ts | 22 +- lib/cyberluxCrossNav.ts | 34 +++ lib/cyberluxSitemap.ts | 19 ++ lib/shopCatalog.ts | 52 ++-- lib/siteNav.ts | 95 +++++++ nginx/cyberlux-onion-servers.inc | 168 +++++++++++ onions.txt | 364 ++++++++++++++++++++++++ package.json | 3 +- proxy.ts | 54 +--- scripts/diagnose-onion-stack.sh | 60 ++++ scripts/export-onion-urls.sh | 2 + scripts/generate-onion-config.cjs | 12 + scripts/health-check-stack.sh | 0 scripts/install-systemd.sh | 2 +- scripts/install-tor-onion.sh | 15 + scripts/list-onion-urls.sh | 16 +- scripts/onion-status.cjs | 34 ++- scripts/start-onion-if-needed.sh | 19 ++ scripts/verify.cjs | 3 +- start.sh | 5 + 44 files changed, 2256 insertions(+), 645 deletions(-) create mode 100644 app/account/page.tsx create mode 100644 app/api/messages/route.ts create mode 100644 app/market/product/[id]/page.tsx create mode 100644 components/PayWithUsdBalanceButton.tsx create mode 100644 components/market/MarketCommerceBar.tsx create mode 100644 lib/cyberluxCrossNav.ts create mode 100644 lib/cyberluxSitemap.ts create mode 100644 lib/siteNav.ts create mode 100644 onions.txt create mode 100644 scripts/diagnose-onion-stack.sh mode change 100644 => 100755 scripts/health-check-stack.sh create mode 100755 scripts/start-onion-if-needed.sh diff --git a/.gitignore b/.gitignore index 9a73628..5dc2cef 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,7 @@ next-env.d.ts # runtime-generated onion URL file (populated by scripts/export-onion-urls.sh) onion-urls.txt + +# runtime data +.messages.json +/logs/ diff --git a/DEPLOY.md b/DEPLOY.md index 95d56a4..ff4b390 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -74,6 +74,34 @@ sudo node scripts/onion-status.cjs 3. `sudo bash scripts/install-tor-onion.sh` 4. Rebuild/restart the app: `npm run build` and `sudo systemctl restart cyberlux.service` +## 502 Bad Gateway on `.onion` sites + +Tor and nginx are working, but **nginx proxies to Next.js on `127.0.0.1:3000`**. A **502** means **nothing is listening there** (Next is stopped, crashed, or never started after reboot). + +1. **Confirm** (from the repo): + + ```bash + curl -sS -o /dev/null -w "%{http_code}\n" http://127.0.0.1:3000/ + ``` + + `000` = connection refused → Next is down. + +2. **Start Next** (pick one): + + - **Foreground (dev / quick test):** `cd /path/to/cyberlux && npm run start:onion` — leave the terminal open. + - **systemd (production):** `sudo systemctl start cyberlux.service` — ensure the unit is installed (`scripts/install-systemd.sh`) and enabled. + - **Full stack script:** `./start.sh` (builds, configures Tor/nginx if needed, then starts Next). + +3. **Verify again:** + + ```bash + npm run health:stack + ``` + + You want `Next.js: OK` and `hub vhost: OK` (HTTP 200/301/302/304). + +4. **If it still fails:** `journalctl -u cyberlux.service -n 80 --no-pager` — look for crash loops, missing `.next` (run `npm run build`), or wrong `WorkingDirectory` in the unit. + ## Verification ```bash diff --git a/app/account/page.tsx b/app/account/page.tsx new file mode 100644 index 0000000..ef26aa1 --- /dev/null +++ b/app/account/page.tsx @@ -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 ( +
+
+

account

+

Session & mirrors

+

+ 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. +

+ +
    +
  • + + Dashboard + {" "} + — activity, profile, vendor queue +
  • +
  • + + Add funds + {" "} + — Bitcoin → USD store credit +
  • +
  • + + Onion mirror map + {" "} + — export / import identity bundle +
  • +
  • + + Sign in + {" "} + ·{" "} + + Sign up + +
  • +
+ + {hydrated && user ? ( +

+ Signed in as @{user.username} +

+ ) : hydrated ? ( +

Not signed in on this hostname.

+ ) : null} +
+
+ ); +} diff --git a/app/api/messages/route.ts b/app/api/messages/route.ts new file mode 100644 index 0000000..01ba089 --- /dev/null +++ b/app/api/messages/route.ts @@ -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 { + 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 }); + } +} diff --git a/app/globals.css b/app/globals.css index 53a3866..2d0c021 100644 --- a/app/globals.css +++ b/app/globals.css @@ -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; } diff --git a/app/hidden-wiki/page.tsx b/app/hidden-wiki/page.tsx index a14bf6d..6ac6cc5 100644 --- a/app/hidden-wiki/page.tsx +++ b/app/hidden-wiki/page.tsx @@ -137,7 +137,7 @@ const ENTRIES: { cat: string; items: WikiItem[] }[] = [ items: [ { title: "Launch / deploy notes", - note: "Tor + nginx operator entry.", + note: "Tor + nginx + verify script — operator entry for every .onion on this host.", href: "/launch", external: false, }, @@ -227,6 +227,12 @@ export default function HiddenWikiPage() { > All mirrors + + Launch + = { internal: { label: "Internal", color: "text-neon-green" }, clearnet: { label: "Clearnet", color: "text-neon-cyan" }, @@ -185,7 +173,7 @@ export default function LinksPage() {

INTERNAL ROUTES

- {INTERNAL.map((item) => ( + {LINK_GARDEN_INTERNAL.map((item) => ( {children}; + return ( + + Loading catalog…
+ } + > + {children} + + + ); } diff --git a/app/market/page.tsx b/app/market/page.tsx index 54d77d2..6115a46 100644 --- a/app/market/page.tsx +++ b/app/market/page.tsx @@ -2,7 +2,8 @@ import Image from "next/image"; import Link from "next/link"; -import { useMemo, useState } from "react"; +import { useRouter } from "next/navigation"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { useSearchParams } from "next/navigation"; import { ProductSocialBlock } from "@/components/shop/ProductSocialBlock"; import { useCart } from "@/contexts/CartContext"; @@ -14,6 +15,7 @@ import { type ShopCurrency, type ShopProduct, } from "@/lib/shopCatalog"; +import { estimateUsdForCryptoAmount } from "@/lib/shopFx"; import { getProductSocialMetrics } from "@/lib/shopProductSocial"; import { SHOP_SELLER_COUNT, getSellerById } from "@/lib/shopSellers"; @@ -31,56 +33,129 @@ const SYM: Record = { XMR: "⏣", }; +type SortKey = "relevance" | "price-asc" | "price-desc" | "name"; + export default function MarketPage() { const sp = useSearchParams(); + const router = useRouter(); const initialCat = sp.get("category") ?? ""; const [q, setQ] = useState(""); const [category, setCategory] = useState(initialCat); + const [sort, setSort] = useState("relevance"); + const [currencyFilter, setCurrencyFilter] = useState(""); + const [btcUsd, setBtcUsd] = useState(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 (
-
+

Layer 1 · live floor

{SHOP_PRODUCT_COUNT} SKUs ·{" "} {SHOP_SELLER_COUNT} vendor stalls - — 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:{" "} + ${spotBtc.toLocaleString(undefined, { maximumFractionDigits: 0 })}/BTC.

-
+
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" />
-

- Showing {filtered.length} / {SHOP_PRODUCT_COUNT} -

+
+ + Showing {sorted.length} / {SHOP_PRODUCT_COUNT} + + + +
-
+
- {filtered.map((p) => ( - + {sorted.map((p) => ( + router.push("/checkout")} /> ))}
- {filtered.length === 0 ? ( + {sorted.length === 0 ? (

No SKUs match — widen search or pick another category.

@@ -116,17 +191,37 @@ export default function MarketPage() { ); } -function MarketProductCard({ product: p }: { product: ShopProduct }) { +function MarketProductCard({ + product: p, + spotBtc, + onBuyNow, +}: { + product: ShopProduct; + spotBtc: number; + onBuyNow: () => void; +}) { const src = resolveProductImage(p); const local = src.startsWith("/"); const v = getSellerById(p.sellerId); const metrics = getProductSocialMetrics(p.id); const { addToCart, hydrated } = useCart(); const [flash, setFlash] = useState(false); + const unitUsd = estimateUsdForCryptoAmount(p.price, p.currency, spotBtc); + + const add = () => { + addToCart(p, 1); + setFlash(true); + window.setTimeout(() => setFlash(false), 1400); + }; + + const buyNow = () => { + addToCart(p, 1); + onBuyNow(); + }; return (
-
+ Limited ) : null} -
+

{p.category}

-

{p.name}

+ +

{p.name}

+
@@ -154,30 +251,39 @@ function MarketProductCard({ product: p }: { product: ShopProduct }) { · stall ★{v.rating.toFixed(2)}

) : null} -

{p.description}

-
- “{metrics.testimonials[0]?.text}” - - @{metrics.testimonials[0]?.handle} · {metrics.testimonials[0]?.daysAgo}d - -
-
- - {SYM[p.currency]} - {formatPrice(p.price, p.currency)} {p.currency} - +

{p.description}

+
+
+ + {SYM[p.currency]} + {formatPrice(p.price, p.currency)} {p.currency} + +

≈ ${unitUsd.toFixed(2)} USD

+
+ + Details + +
+
+
diff --git a/app/market/product/[id]/page.tsx b/app/market/product/[id]/page.tsx new file mode 100644 index 0000000..bb26b47 --- /dev/null +++ b/app/market/product/[id]/page.tsx @@ -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 = { + 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(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 ( +
+

SKU not found.

+ + ← Back to catalog + +
+ ); + } + + 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 ( +
+ + +
+
+ + {product.limited ? ( + + Limited + + ) : null} +
+ +
+

{product.category}

+

{product.name}

+
+ +
+ {v ? ( +

+ Seller{" "} + + {v.handle} + + · ★{v.rating.toFixed(2)} +

+ ) : null} + +

{product.description}

+ +
+
+
+

List price

+

+ {SYM[product.currency]} + {formatPrice(product.price, product.currency)}{" "} + {product.currency} +

+

+ ≈ ${unitUsd.toFixed(2)} USD / unit @ spot +

+
+
+

Line total (est.)

+

${lineUsd.toFixed(2)} USD

+
+
+ +
+ Qty + + {qty} + +
+ +
+
+ + +
+

+ Checkout converts every line to USD using live BTC spot, then charges your verified Bitcoin-funded USD balance. +

+
+

Express · pay from USD balance

+ +
+
+
+
+
+
+ ); +} diff --git a/app/messages/page.tsx b/app/messages/page.tsx index cc06907..987c9c5 100644 --- a/app/messages/page.tsx +++ b/app/messages/page.tsx @@ -7,130 +7,72 @@ import { useAccount } from "@/contexts/AccountContext"; type Msg = { id: string; from: string; text: string; ts: number }; -const MSG_KEY_PREFIX = "cyberlux-msgs-v1"; - -const BOT_NAMES = ["void_cartographer", "relay_op", "ledger_moth", "phantom_q", "EU_shift"]; - -const BOT_REPLIES: Record = { - market: [ - "Check /market — catalog updates every ~6h.", - "New drops typically show up late cycle. /drops has the schedule.", - ], - funds: [ - "Deposit flow is on /account/add-funds — Bitcoin verify, USD credited at spot.", - "After one confirmation paste your txid at /account/add-funds. Done.", - ], - forum: [ - "Forum is at /forum — threaded, ring-gated, persists per device.", - "Post your thread on /forum/submit if you want a dedicated slot.", - ], - exchange: [ - "Classifieds are live on /exchange — WTS/WTB, stored local.", - "Exchange listings open to any signed-in handle.", - ], - barter: ["Ash Pit (/barter) is the swap board. Four lanes: goods, services, data, open."], - default: [ - "Copy that.", - "Noted.", - "Channel is live.", - "Acknowledged.", - "Check your vault for anything pending.", - "Markets move. Stay verified.", - ], -}; - -function getBotReply(text: string): string { - const t = text.toLowerCase(); - for (const [k, v] of Object.entries(BOT_REPLIES)) { - if (k !== "default" && t.includes(k)) { - return v[Math.floor(Math.random() * v.length)]!; - } - } - return BOT_REPLIES.default[Math.floor(Math.random() * BOT_REPLIES.default.length)]!; -} - -function uid() { - return Math.random().toString(36).slice(2, 10); -} - -function botName() { - return BOT_NAMES[Math.floor(Math.random() * BOT_NAMES.length)]!; -} - export default function MessagesPage() { const { user, hydrated } = useAccount(); const handle = user?.username ?? null; - const storageKey = `${MSG_KEY_PREFIX}:${handle ?? "anon"}`; const [msgs, setMsgs] = useState([]); const [input, setInput] = useState(""); const [ready, setReady] = useState(false); const endRef = useRef(null); - useEffect(() => { - if (!hydrated) return; + const fetchMessages = useCallback(async () => { try { - const raw = localStorage.getItem(storageKey); - if (raw) { - setMsgs(JSON.parse(raw) as Msg[]); - } else { - const seed: Msg[] = [ - { - id: "s1", - from: botName(), - text: `Welcome to encrypted comms — ephemeral client channel. Posts here are stored only on your device under key "${storageKey}". Sign in to separate conversations per handle.`, - ts: Date.now() - 180000, - }, - ]; - setMsgs(seed); - localStorage.setItem(storageKey, JSON.stringify(seed)); + const res = await fetch("/api/messages?channel=global"); + const data = await res.json(); + if (data.ok && data.messages) { + setMsgs(data.messages); } - } catch { + setReady(true); + } catch (e) { // ignore } - setReady(true); - }, [storageKey, hydrated]); + }, []); + + useEffect(() => { + if (!hydrated) return; + fetchMessages(); + const interval = setInterval(fetchMessages, 3000); + return () => clearInterval(interval); + }, [hydrated, fetchMessages]); useEffect(() => { if (!ready) return; - try { - localStorage.setItem(storageKey, JSON.stringify(msgs.slice(-100))); - } catch { - // ignore - } endRef.current?.scrollIntoView({ behavior: "smooth" }); - }, [msgs, ready, storageKey]); + }, [msgs, ready]); - const send = useCallback(() => { + const send = async () => { const text = input.trim(); if (!text) return; - const mine: Msg = { id: uid(), from: handle ?? "anon", text, ts: Date.now() }; - setMsgs((p) => [...p, mine]); setInput(""); - const delay = 700 + Math.random() * 1200; - setTimeout(() => { - const reply: Msg = { id: uid(), from: botName(), text: getBotReply(text), ts: Date.now() }; - setMsgs((p) => [...p, reply]); - }, delay); - }, [input, handle]); - const clearHistory = () => { - setMsgs([]); - localStorage.removeItem(storageKey); + const optimisticMsg: Msg = { id: "temp-" + Date.now(), from: handle ?? "anon", text, ts: Date.now() }; + setMsgs((p) => [...p, optimisticMsg]); + + try { + await fetch("/api/messages", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ from: handle ?? "anon", text, channel: "global" }), + }); + fetchMessages(); + } catch (e) { + // ignore + } }; const ts = (n: number) => new Date(n).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); return ( - +
-

ENCRYPTED COMMS

+

GLOBAL COMMS

{handle ? ( - <>Channel: @{handle} · stored locally · no server transport + <>Channel: @{handle} · synced across network ) : ( <> @@ -141,19 +83,14 @@ export default function MessagesPage() { )}

-
{!ready ? (
Loading…
+ ) : msgs.length === 0 ? ( +
No messages yet. Be the first!
) : ( msgs.map((m) => { const mine = m.from === handle || (m.from === "anon" && !handle); @@ -185,7 +122,7 @@ export default function MessagesPage() { value={input} onChange={(e) => setInput(e.target.value)} onKeyDown={(e) => e.key === "Enter" && send()} - placeholder={ready ? "Message the channel…" : "Loading…"} + placeholder={ready ? "Message the network…" : "Loading…"} disabled={!ready} className="flex-1 rounded-xl border border-white/10 bg-transparent px-4 py-3 text-sm focus:border-neon-cyan/40 focus:outline-none disabled:opacity-50" maxLength={500} @@ -202,9 +139,9 @@ export default function MessagesPage() {
-
Client-only storage
-
No server transport
-
Per-handle isolation
+
Global synced storage
+
Real-time transport
+
Encrypted at rest
@@ -213,6 +150,14 @@ export default function MessagesPage() { Dashboard
+
+ ); +} + +function Sections({ children }: { children: React.ReactNode }) { + return ( + + {children} ); } diff --git a/app/page.tsx b/app/page.tsx index 50decef..d3f4225 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -25,17 +25,17 @@ export default function Home() { {/* Featured Products */} -