Add persistent onion key backup and restore, improve startup resilience, and flesh out the major site verticals with richer navigation, search coverage, and operator documentation. Made-with: Cursor
120 lines
5.4 KiB
TypeScript
120 lines
5.4 KiB
TypeScript
"use client";
|
|
|
|
import { useMemo } from "react";
|
|
import { SHOP_PRODUCTS, shopCategoryCounts, type ShopCurrency } from "@/lib/shopCatalog";
|
|
|
|
function currencySymbol(c: ShopCurrency): string {
|
|
if (c === "BTC") return "₿";
|
|
if (c === "ETH") return "Ξ";
|
|
if (c === "XMR") return "⏣";
|
|
return "$";
|
|
}
|
|
|
|
export default function MarketAnalyticsPage() {
|
|
const { byCategory, byCurrency, avgPriceUsdish, limitedCount } = useMemo(() => {
|
|
const cats = shopCategoryCounts();
|
|
const cur: Record<ShopCurrency, number> = { BTC: 0, ETH: 0, USD: 0, XMR: 0 };
|
|
let usdSum = 0;
|
|
let n = 0;
|
|
let lim = 0;
|
|
for (const p of SHOP_PRODUCTS) {
|
|
cur[p.currency]++;
|
|
if (p.currency === "USD") {
|
|
usdSum += p.price;
|
|
n++;
|
|
} else if (p.currency === "BTC") {
|
|
usdSum += p.price * 65_000;
|
|
n++;
|
|
} else if (p.currency === "ETH") {
|
|
usdSum += p.price * 3_500;
|
|
n++;
|
|
} else {
|
|
usdSum += p.price * 165;
|
|
n++;
|
|
}
|
|
if (p.limited) lim++;
|
|
}
|
|
const maxCat = Math.max(1, ...cats.map((c) => c.count));
|
|
const maxCur = Math.max(1, ...Object.values(cur));
|
|
return {
|
|
byCategory: cats.map((c) => ({ ...c, pct: Math.round((c.count / maxCat) * 100) })),
|
|
byCurrency: (Object.entries(cur) as [ShopCurrency, number][]).map(([k, v]) => ({
|
|
k,
|
|
v,
|
|
pct: Math.round((v / maxCur) * 100),
|
|
})),
|
|
avgPriceUsdish: n ? usdSum / n : 0,
|
|
limitedCount: lim,
|
|
};
|
|
}, []);
|
|
|
|
return (
|
|
<div className="space-y-10 font-mono text-[#c8ffd8]">
|
|
<header>
|
|
<p className="text-[10px] uppercase tracking-[0.45em] text-[#00ff41]/45">Layer 2 · floor telemetry</p>
|
|
<h1 className="mt-2 text-2xl font-black tracking-tight text-[#7af598] md:text-3xl">Analytics</h1>
|
|
<p className="mt-3 max-w-3xl text-sm leading-relaxed text-[#5a8f6a]">
|
|
Derived from the live catalog in <code className="text-[#7af598]">shopCatalog</code> — not a third-party charting plugin. Bars are CSS; numbers update when SKUs change. Use this view to explain category heat and currency mix on vendor calls or mirror status pages.
|
|
</p>
|
|
</header>
|
|
|
|
<section className="grid gap-6 md:grid-cols-3">
|
|
<div className="rounded border border-[#00ff41]/25 bg-[#050a08]/90 p-5">
|
|
<p className="text-[10px] uppercase tracking-widest text-[#00ff41]/50">SKUs</p>
|
|
<p className="mt-1 text-3xl font-black text-[#b4ffcc]">{SHOP_PRODUCTS.length}</p>
|
|
</div>
|
|
<div className="rounded border border-[#00ff41]/25 bg-[#050a08]/90 p-5">
|
|
<p className="text-[10px] uppercase tracking-widest text-[#00ff41]/50">Limited drops</p>
|
|
<p className="mt-1 text-3xl font-black text-[#ff9a9a]">{limitedCount}</p>
|
|
</div>
|
|
<div className="rounded border border-[#00ff41]/25 bg-[#050a08]/90 p-5">
|
|
<p className="text-[10px] uppercase tracking-widest text-[#00ff41]/50">Avg notional (rough USD)</p>
|
|
<p className="mt-1 text-3xl font-black text-[#7af598]">
|
|
${avgPriceUsdish.toFixed(0)}
|
|
</p>
|
|
<p className="mt-2 text-[9px] text-[#4a6b55]">BTC/ETH/XMR converted with static fiction rates for display only.</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section className="rounded border border-[#00ff41]/20 p-6">
|
|
<h2 className="text-xs font-black uppercase tracking-widest text-[#00ff41]/70">Category mix</h2>
|
|
<ul className="mt-4 space-y-3">
|
|
{byCategory.map((c) => (
|
|
<li key={c.name} className="flex items-center gap-3 text-xs">
|
|
<span className="w-8 shrink-0 text-lg">{c.icon}</span>
|
|
<span className="w-36 shrink-0 font-bold text-[#b4ffcc]">{c.name}</span>
|
|
<div className="h-2 min-w-0 flex-1 overflow-hidden rounded bg-[#0a120d]">
|
|
<div className="h-full bg-gradient-to-r from-[#00ff41]/20 to-[#7af598]" style={{ width: `${c.pct}%` }} />
|
|
</div>
|
|
<span className="w-10 shrink-0 text-right text-[#6a9b7a]">{c.count}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</section>
|
|
|
|
<aside className="rounded border border-dashed border-[#00ff41]/25 p-4 text-[10px] leading-relaxed text-[#4a6b55]">
|
|
<strong className="text-[#5a8f6a]">Telemetry vocabulary:</strong> “heat” is relative SKU density per category, “mix” is currency tag share, “notional” is a
|
|
fiction USD bridge using static BTC/ETH/XMR anchors — useful for comparing stalls, not for pricing legal tender. All terms index the{" "}
|
|
<strong className="text-[#6a9b7a]">Void crawler</strong> when mirrored on search-enabled onions.
|
|
</aside>
|
|
|
|
<section className="rounded border border-[#00ff41]/20 p-6">
|
|
<h2 className="text-xs font-black uppercase tracking-widest text-[#00ff41]/70">Currency mix</h2>
|
|
<ul className="mt-4 space-y-3">
|
|
{byCurrency.map(({ k, v, pct }) => (
|
|
<li key={k} className="flex items-center gap-3 text-xs">
|
|
<span className="w-10 shrink-0 font-bold text-[#9dffc4]">
|
|
{currencySymbol(k)} {k}
|
|
</span>
|
|
<div className="h-2 min-w-0 flex-1 overflow-hidden rounded bg-[#0a120d]">
|
|
<div className="h-full bg-[#00ff41]/35" style={{ width: `${pct}%` }} />
|
|
</div>
|
|
<span className="w-10 shrink-0 text-right text-[#6a9b7a]">{v}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|