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:
69
components/shop/VendorShelfRow.tsx
Normal file
69
components/shop/VendorShelfRow.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user