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
This commit is contained in:
drjones
2026-04-24 23:23:48 -07:00
parent cb072437d0
commit 04d64fb993
44 changed files with 2256 additions and 645 deletions

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