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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<ShopCurrency, string> = {
|
||||
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<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 A–Z</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,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 (
|
||||
<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 +233,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 +251,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>
|
||||
|
||||
220
app/market/product/[id]/page.tsx
Normal file
220
app/market/product/[id]/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user