Harden onion boot flow and deepen site surfaces

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
This commit is contained in:
drjones
2026-04-07 21:35:52 -07:00
parent 52432dccfa
commit 78a071ba02
162 changed files with 21692 additions and 39 deletions

View File

@@ -0,0 +1,75 @@
"use client";
import type { ProductSocialMetrics } from "@/lib/shopProductSocial";
function Stars({ value, size = "sm" }: { value: number; size?: "sm" | "xs" }) {
const rounded = Math.round(value * 2) / 2;
const full = Math.floor(rounded);
const half = rounded - full >= 0.5;
const empty = 5 - full - (half ? 1 : 0);
const c = size === "xs" ? "text-[10px]" : "text-xs";
return (
<span className={`${c} tracking-tight text-amber-400`} aria-hidden>
{"★".repeat(full)}
{half ? <span className="text-amber-400/60"></span> : null}
<span className="text-foreground/22">{"☆".repeat(Math.max(0, empty))}</span>
</span>
);
}
export function ProductSocialBlock({
metrics,
compact = false,
}: {
metrics: ProductSocialMetrics;
compact?: boolean;
}) {
const { avgRating, reviewCount, purchaseCount, boughtLast24h, testimonials } = metrics;
if (compact) {
return (
<div className="space-y-1">
<div className="flex flex-wrap items-center gap-x-2 gap-y-0.5">
<Stars value={avgRating} size="xs" />
<span className="font-mono text-[11px] font-bold tabular-nums text-foreground/90">{avgRating.toFixed(2)}</span>
<span className="font-mono text-[10px] text-foreground/45">({reviewCount.toLocaleString()} reviews)</span>
</div>
<p className="font-mono text-[10px] text-emerald-400/80">
{purchaseCount.toLocaleString()} sold · <span className="text-emerald-300/90">{boughtLast24h}</span> in 24h
</p>
</div>
);
}
return (
<div className="space-y-3">
<div className="flex flex-wrap items-center gap-2">
<Stars value={avgRating} />
<span className="font-orbitron text-sm font-bold tabular-nums">{avgRating.toFixed(2)}</span>
<span className="text-xs text-foreground/50">({reviewCount.toLocaleString()} reviews)</span>
</div>
<p className="font-mono text-[11px] text-emerald-400/85">
<span className="text-foreground/55">Purchases:</span>{" "}
<strong className="text-emerald-300">{purchaseCount.toLocaleString()}</strong> total ·{" "}
<strong className="text-emerald-300">{boughtLast24h}</strong> bought in last 24h
</p>
<div className="space-y-2 border-t border-white/10 pt-3">
<p className="font-mono text-[9px] font-bold uppercase tracking-widest text-foreground/40">Recent buyers</p>
{testimonials.slice(0, 2).map((t, i) => (
<blockquote
key={`${t.handle}-${i}`}
className="rounded-lg border border-white/5 bg-white/[0.03] px-3 py-2 text-xs leading-snug text-foreground/75"
>
<div className="mb-1 flex flex-wrap items-center justify-between gap-2 font-mono text-[10px] text-foreground/45">
<span className="text-neon-cyan/80">@{t.handle}</span>
<span>
<span className="text-amber-400">{t.rating}</span> · {t.daysAgo}d ago
</span>
</div>
{t.text}
</blockquote>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,69 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import { useState } from "react";
import { useCart } from "@/contexts/CartContext";
import { ProductSocialBlock } from "@/components/shop/ProductSocialBlock";
import { resolveProductImage, type ShopCurrency, type ShopProduct } from "@/lib/shopCatalog";
import { getProductSocialMetrics } from "@/lib/shopProductSocial";
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 function VendorShelfRow({ product }: { product: ShopProduct }) {
const { addToCart, hydrated } = useCart();
const [flash, setFlash] = useState(false);
const src = resolveProductImage(product);
const metrics = getProductSocialMetrics(product.id);
return (
<li className="flex flex-col gap-4 rounded-lg border border-orange-950/70 bg-[#080605] p-4 sm:flex-row">
<div className="relative h-28 w-full shrink-0 overflow-hidden rounded border border-orange-900/30 bg-black sm:h-28 sm:w-28">
<Image src={src} alt="" fill className="object-cover" sizes="112px" unoptimized={src.startsWith("/")} />
</div>
<div className="min-w-0 flex-1">
<p className="font-mono text-[10px] uppercase tracking-wider text-orange-500/60">{product.category}</p>
<p className="font-semibold text-orange-50">{product.name}</p>
<div className="mt-2 text-[#c4b5a0]">
<ProductSocialBlock metrics={metrics} compact />
</div>
<p className="mt-2 line-clamp-2 text-xs text-orange-200/65">{product.description}</p>
<p className="mt-2 font-mono text-sm text-orange-300">
{SYM[product.currency]}
{formatPrice(product.price, product.currency)} {product.currency}
</p>
<div className="mt-3 space-y-2 border-t border-orange-950/50 pt-3">
<p className="font-mono text-[9px] font-bold uppercase tracking-widest text-orange-500/50">Verified note</p>
<p className="text-xs italic text-orange-200/60">{metrics.testimonials[0]?.text}</p>
</div>
<div className="mt-4 flex flex-wrap gap-2">
<button
type="button"
disabled={!hydrated}
onClick={() => {
addToCart(product, 1);
setFlash(true);
window.setTimeout(() => setFlash(false), 1500);
}}
className="rounded border border-orange-500 bg-orange-500/90 px-4 py-2 font-mono text-xs font-bold uppercase text-black hover:bg-orange-400 disabled:opacity-50"
>
{flash ? "Added ✓" : "Add to cart"}
</button>
<Link
href="/checkout"
className="rounded border border-orange-800 px-4 py-2 font-mono text-xs text-orange-200 hover:border-orange-600"
>
Cart
</Link>
</div>
</div>
</li>
);
}