import { useState } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { nip19 } from 'nostr-tools'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter, } from '@/components/ui/dialog'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator, } from '@/components/ui/dropdown-menu'; import { Textarea } from '@/components/ui/textarea'; import { Label } from '@/components/ui/label'; import { useAuthor } from '@/hooks/useAuthor'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useCashu } from '@/hooks/wallet/useCashu'; import { useToast } from '@/hooks/useToast'; import { useNostrPublish } from '@/hooks/useNostrPublish'; import { useListingReports } from '@/hooks/useListingReports'; import { useSellerInfo } from '@/hooks/useSellerInfo'; import { useTrustScore } from '@/hooks/useTrustScore'; import { genUserName } from '@/lib/genUserName'; import { TrustBadge } from '@/components/TrustBadge'; import { MessageCircle, Clock, Search, Tag, Flag, MoreVertical, ShieldCheck, ShieldAlert, Calendar, Package, AlertTriangle, CheckCircle, } from 'lucide-react'; import { formatDistanceToNow } from 'date-fns'; import type { MarketplaceItem } from '@/lib/types'; import { GEAR_CONDITIONS, GEAR_CATEGORIES, PRICE_TYPES, LISTING_REPORT_REASONS, formatPrice } from '@/lib/types'; interface MarketplaceItemCardProps { item: MarketplaceItem; compact?: boolean; } export function MarketplaceItemCard({ item, compact = false }: MarketplaceItemCardProps) { const { user } = useCurrentUser(); const { toast } = useToast(); const navigate = useNavigate(); const { mutateAsync: publishEvent } = useNostrPublish(); const author = useAuthor(item.pubkey); const metadata = author.data?.metadata; const displayName = metadata?.name ?? genUserName(item.pubkey); const avatar = metadata?.picture; const npub = nip19.npubEncode(item.pubkey); // Fetch reports for this listing const { data: reportData } = useListingReports(item.id, item.pubkey); // Fetch seller info const { data: sellerInfo } = useSellerInfo(item.pubkey); // Fetch trust score const { data: trustScore } = useTrustScore(item.pubkey); // Cashu wallet integration const { totalBalance: cashuBalance, isInitialized: hasCashu, coco } = useCashu(); const [showPaymentDialog, setShowPaymentDialog] = useState(false); const [showReportDialog, setShowReportDialog] = useState(false); const [reportReason, setReportReason] = useState(''); const [reportDetails, setReportDetails] = useState(''); const [isReporting, setIsReporting] = useState(false); const [isPaying, setIsPaying] = useState(false); // Format price display const priceDisplay = item.price ? `${formatPrice(item.price, item.priceType)}${item.price.frequency ? `/${item.price.frequency}` : ''}` : formatPrice(undefined, item.priceType); const timeAgo = formatDistanceToNow(new Date(item.createdAt * 1000), { addSuffix: true }); const primaryImage = item.images[0]; const isWTB = item.listingType === 'wtb'; // Expiration is stored as unix timestamp in seconds const isExpired = item.expiration ? item.expiration < Math.floor(Date.now() / 1000) : false; // Get condition info const conditionInfo = item.condition ? GEAR_CONDITIONS[item.condition] : null; // Get category label const categoryLabel = item.category?.find((c) => GEAR_CATEGORIES[c as keyof typeof GEAR_CATEGORIES]) ? GEAR_CATEGORIES[item.category.find((c) => GEAR_CATEGORIES[c as keyof typeof GEAR_CATEGORIES]) as keyof typeof GEAR_CATEGORIES] : null; // Get price type label const priceTypeLabel = item.priceType ? PRICE_TYPES[item.priceType]?.label : null; // Calculate price in sats (rough estimate: $1 = 2500 sats at $40k/BTC) const priceInSats = item.price?.amount ? Math.round(parseFloat(item.price.amount) * 2500) : 0; const canPayWithCashu = hasCashu && cashuBalance >= priceInSats && priceInSats > 0; // Seller verification status const isVerifiedSeller = sellerInfo?.hasNip05 || false; const isHighTrust = (trustScore || 0) >= 60; const accountAgeDays = sellerInfo?.accountAge || 0; const previousListings = sellerInfo?.listingCount || 0; // Report status const reportCount = reportData?.count || 0; const shouldHide = reportData?.shouldHide || false; const handleCashuPayment = async () => { if (!coco || !user) return; setIsPaying(true); try { toast({ title: 'Payment Feature Coming Soon', description: `Cashu peer-to-peer payments will be available soon! For now, please contact the seller directly.`, }); setShowPaymentDialog(false); } catch (error) { toast({ variant: 'destructive', title: 'Payment Failed', description: error instanceof Error ? error.message : 'Failed to process payment', }); } finally { setIsPaying(false); } }; const handleReport = async () => { if (!user || !reportReason) { toast({ title: 'Please select a reason', variant: 'destructive' }); return; } setIsReporting(true); try { // Publish NIP-56 report event (kind 1984) await publishEvent({ kind: 1984, content: reportDetails.trim(), tags: [ ['e', item.id, '', reportReason], ['p', item.pubkey], ['L', 'skate-spots.report'], ['l', reportReason, 'skate-spots.report'], ], }); toast({ title: 'Report submitted', description: 'Thank you for helping keep the marketplace safe.', }); setShowReportDialog(false); setReportReason(''); setReportDetails(''); } catch (error) { toast({ variant: 'destructive', title: 'Failed to submit report', description: error instanceof Error ? error.message : 'Please try again', }); } finally { setIsReporting(false); } }; // Don't render expired or heavily reported listings if (isExpired || shouldHide) { return null; } const isOwnListing = user?.pubkey === item.pubkey; return ( {/* Report warning badge */} {reportCount > 0 && reportCount < 3 && (
{reportCount} report{reportCount > 1 ? 's' : ''}
)}
{/* Product Image */}
{primaryImage ? (
{item.title}
) : (
{isWTB ? '🔍' : '🛹'}
)}
{/* Content */}
{/* Listing Type Badge + Title */}
{isWTB && ( WANT TO BUY )}

{item.title}

{priceDisplay} {priceTypeLabel && priceTypeLabel !== 'Fixed Price' && (
{priceTypeLabel}
)}
{/* Actions dropdown */} {user && !isOwnListing && ( setShowReportDialog(true)} className="text-red-400 focus:text-red-400" > Report Listing )}
{/* Condition, Size, Category Badges */}
{conditionInfo && ( {conditionInfo.emoji} {conditionInfo.label} )} {item.size && ( {item.size} )} {categoryLabel && ( {categoryLabel} )}
{/* Description */} {(item.summary || item.description) && !compact && (

{item.summary || item.description}

)} {/* Seller Info with enhanced trust indicators */}
{displayName.slice(0, 2).toUpperCase()}
{displayName} • {timeAgo}
{/* Trust indicators */}
{isVerifiedSeller && ( Verified )} {isHighTrust && !isVerifiedSeller && ( Trusted )}
{/* Seller stats (account age, listings) */} {!compact && sellerInfo && (
{accountAgeDays > 0 && (
{accountAgeDays < 30 ? `${accountAgeDays}d old` : accountAgeDays < 365 ? `${Math.floor(accountAgeDays / 30)}mo old` : `${Math.floor(accountAgeDays / 365)}y old`}
)} {previousListings > 1 && (
{previousListings} listings
)}
)} {/* Availability */} {item.availability && (
Usually here {item.availability}
)} {/* Status Badge */} {item.status && item.status !== 'active' && ( {item.status.toUpperCase()} )} {/* Action Buttons */}
{/* Pay with Cashu button */} {user && !isOwnListing && canPayWithCashu && item.status === 'active' && !isWTB && ( )} {/* Contact button */}
{/* Payment Confirmation Dialog */} Confirm Payment Review the payment details before confirming
{/* Item details */}
{primaryImage && ( {item.title} )}

{item.title}

Sold by {displayName}

{/* Payment details */}
Price: {priceDisplay}
Amount (sats): {priceInSats.toLocaleString()} sats
Your Cashu Balance: {cashuBalance.toLocaleString()} sats
After Payment: {(cashuBalance - priceInSats).toLocaleString()} sats

⚠️ Peer-to-peer payments are coming soon! This feature will allow you to send Cashu tokens directly to the seller. For now, please contact the seller to arrange payment.

{/* Report Dialog */} Report Listing Help keep the marketplace safe by reporting inappropriate or misleading listings.
{Object.entries(LISTING_REPORT_REASONS).map(([key, { label, description }]) => ( ))}