Files
dark-lord/components/ProductCard.tsx
drjones 78a071ba02 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
2026-04-07 21:35:52 -07:00

189 lines
6.7 KiB
TypeScript

"use client";
import { useMemo, useState } from "react";
import Image from "next/image";
import Link from "next/link";
import { useCart } from "@/contexts/CartContext";
import { ProductSocialBlock } from "@/components/shop/ProductSocialBlock";
import { SHOP_PRODUCTS, type ShopProduct } from "@/lib/shopCatalog";
import { getProductSocialMetrics } from "@/lib/shopProductSocial";
interface ProductCardProps {
id: string;
name: string;
description: string;
price: number;
currency: "BTC" | "ETH" | "USD" | "XMR";
category: string;
imageUrl: string;
sellerHandle: string;
sellerSlug: string;
limited?: boolean;
glowColor?: string;
/** When set, add-to-cart uses this row; otherwise looks up by `id` in SHOP_PRODUCTS */
shopProduct?: ShopProduct;
}
const ProductCard = ({
id,
name,
description,
price,
currency,
category,
imageUrl,
sellerHandle,
sellerSlug,
limited = false,
glowColor: _glow = "neon-cyan",
shopProduct,
}: ProductCardProps) => {
void _glow;
const [isHovered, setIsHovered] = useState(false);
const [isFavorite, setIsFavorite] = useState(false);
const [justAdded, setJustAdded] = useState(false);
const { addToCart, hydrated: cartReady } = useCart();
const metrics = useMemo(() => getProductSocialMetrics(id), [id]);
const currencySymbols = {
BTC: "₿",
ETH: "Ξ",
USD: "$",
XMR: "⏣",
};
const formatPrice = (p: number) => {
if (currency === "BTC") return p.toFixed(6);
if (currency === "ETH") return p.toFixed(4);
if (currency === "XMR") return p.toFixed(3);
return p.toFixed(2);
};
const resolveProduct = (): ShopProduct | undefined => shopProduct ?? SHOP_PRODUCTS.find((x) => x.id === id);
const handleAddToCart = () => {
const p = resolveProduct();
if (!p) return;
addToCart(p, 1);
setJustAdded(true);
window.setTimeout(() => setJustAdded(false), 1800);
};
return (
<div
className="group relative overflow-hidden rounded-2xl border border-white/10 transition-all duration-500 hover:scale-[1.02] hover:shadow-2xl hover:shadow-neon-cyan/10"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<div
className={`absolute -inset-1 -z-10 rounded-2xl opacity-0 blur-xl transition-opacity duration-500 ${
isHovered ? "bg-neon-cyan/30 opacity-20" : ""
}`}
/>
{limited && (
<div className="absolute left-4 top-4 z-10">
<div className="relative">
<div className="animate-glow rounded-full bg-gradient-to-r from-neon-pink to-neon-purple px-3 py-1 text-xs font-bold text-background">
LIMITED
</div>
<div className="absolute -inset-1 -z-10 animate-pulse rounded-full bg-neon-pink blur-md opacity-30"></div>
</div>
</div>
)}
<button
type="button"
className="absolute right-4 top-4 z-10 glass rounded-full p-2"
onClick={() => setIsFavorite(!isFavorite)}
aria-label={isFavorite ? "Remove from favorites" : "Add to favorites"}
>
<span className={`text-lg ${isFavorite ? "text-neon-pink" : "text-foreground/60"}`}>
{isFavorite ? "❤️" : "🤍"}
</span>
</button>
<div className="relative aspect-square overflow-hidden">
<Image
src={imageUrl}
alt={name}
fill
className="object-cover transition-transform duration-700 group-hover:scale-110"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
unoptimized={imageUrl.startsWith("/")}
/>
<div className="absolute inset-0 bg-gradient-to-t from-background/80 via-transparent to-transparent opacity-0 transition-opacity group-hover:opacity-100" />
</div>
<div className="glass border-t border-white/10 p-6 backdrop-blur-sm">
<div className="mb-3">
<ProductSocialBlock metrics={metrics} compact />
</div>
<div className="mb-2 flex items-center justify-between gap-2">
<span className="rounded-full bg-white/5 px-3 py-1 text-xs font-medium text-neon-cyan">{category}</span>
<div className="flex flex-shrink-0 items-center gap-1">
<span className="font-orbitron text-xl font-bold text-foreground md:text-2xl">
{currencySymbols[currency]}
{formatPrice(price)}
</span>
<span className="text-sm text-foreground/50">{currency}</span>
</div>
</div>
<h3 className="mb-2 font-orbitron text-xl font-bold">{name}</h3>
<p className="mb-1 text-xs text-foreground/50">
Seller:{" "}
<Link href={`/vendor/${sellerSlug}`} className="text-neon-cyan hover:text-neon-purple hover:underline">
{sellerHandle}
</Link>
</p>
<p className="mb-4 line-clamp-3 text-sm text-foreground/70">{description}</p>
<div className="mb-4 space-y-2 border-t border-white/10 pt-4">
<p className="font-mono text-[9px] font-bold uppercase tracking-widest text-foreground/35">Verified chatter</p>
{metrics.testimonials.slice(0, 1).map((t, i) => (
<p key={i} className="text-xs italic leading-relaxed text-foreground/55">
{t.text}
<span className="mt-1 block font-mono text-[10px] not-italic text-neon-cyan/70">
@{t.handle} · {t.rating} · {t.daysAgo}d ago
</span>
</p>
))}
</div>
<div className="flex items-center gap-3">
<button
type="button"
onClick={handleAddToCart}
disabled={!cartReady || !resolveProduct()}
className="flex-1 rounded-full bg-gradient-to-r from-neon-cyan to-neon-purple py-3 text-sm font-bold text-background transition-transform enabled:hover:scale-[1.02] disabled:opacity-50"
>
{justAdded ? "ADDED ✓" : "ADD TO CART"}
</button>
<Link
href="/checkout"
className="glass flex items-center justify-center rounded-full p-3 transition-colors hover:bg-white/10"
title="Checkout"
>
<span className="text-lg">🛒</span>
</Link>
</div>
<div className="mt-4 flex items-center justify-between text-xs text-foreground/50">
<div className="flex items-center gap-2">
<div className="h-2 w-2 rounded-full bg-neon-green"></div>
<span>In stock · Ships discreetly</span>
</div>
<div className="flex items-center gap-1">
<span>🔐</span>
<span>Encrypted</span>
</div>
</div>
</div>
</div>
);
};
export default ProductCard;