import { useState } from 'react'; import { 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 { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { useAuthor } from '@/hooks/useAuthor'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useDeleteEntity } from '@/hooks/useDeleteEntity'; import { useMarkListingSold } from '@/hooks/useMarkListingSold'; import { useToast } from '@/hooks/useToast'; import { isAdminPubkey } from '@/lib/admins'; import { genUserName } from '@/lib/genUserName'; import { getLastLocation } from '@/lib/lastLocation'; import { itemDistanceKm } from '@/lib/sortOptions'; import { getSkateboardRating, getTrustLevel, SELLER_TRUST_BADGES } from '@/lib/skateboardRating'; import { GEAR_CONDITIONS, GEAR_CATEGORIES, formatPrice, type MarketplaceItem } from '@/lib/types'; import { MessageCircle, MapPin, Search, CheckCircle, ShieldCheck, AlertTriangle, MoreVertical, Flag, Trash2, RotateCcw, Loader2 } from 'lucide-react'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator } from '@/components/ui/dropdown-menu'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog'; import { ReportDialog } from '@/components/ReportDialog'; import { formatDistanceToNow } from 'date-fns'; import { useTranslation } from '@/contexts/language'; interface GearSwapItemCardProps { item: MarketplaceItem; trustScore?: number; salesCount: number; isFollowing?: boolean; } export function GearSwapItemCard({ item, trustScore, salesCount }: GearSwapItemCardProps) { const { t } = useTranslation(); const { user } = useCurrentUser(); const navigate = useNavigate(); // Distance from the viewer's last known location (cache only — no GPS call). const viewerLoc = getLastLocation(); const distanceKm = viewerLoc ? itemDistanceKm(item, viewerLoc) : null; const author = useAuthor(item.pubkey); const { mutateAsync: deleteEntity, isPending: isRemoving } = useDeleteEntity(); const { markSold, relist, isPending: isUpdatingStatus } = useMarkListingSold(); const { toast } = useToast(); const metadata = author.data?.metadata; const displayName = metadata?.name ?? genUserName(item.pubkey); const avatar = metadata?.picture; const [showReportDialog, setShowReportDialog] = useState(false); const [showRemoveDialog, setShowRemoveDialog] = useState(false); const npub = nip19.npubEncode(item.pubkey); const isOwnListingCheck = user?.pubkey === item.pubkey; const isAdmin = isAdminPubkey(user?.pubkey); // Used by both the owner ("Delete listing") and admin ("Remove") flows — // the wording adapts to which one is acting. const ownerDeleting = isOwnListingCheck; const handleRemove = async () => { try { await deleteEntity({ event: item.event, reason: ownerDeleting ? `Deleted listing "${item.title}"` : `SkateSpots admin removed listing "${item.title}"`, }); toast({ title: ownerDeleting ? t('marketUi.card.listingDeleted') : t('marketUi.card.listingRemoved'), description: ownerDeleting ? t('marketUi.card.listingDeletedBody') : t('marketUi.card.listingRemovedBody'), }); setShowRemoveDialog(false); } catch (error) { toast({ title: t('marketUi.card.removeFailed'), description: error instanceof Error ? error.message : t('marketUi.card.somethingWrong'), variant: 'destructive', }); } }; // Format price display const priceDisplay = formatPrice(item.price, item.priceType); const timeAgo = formatDistanceToNow(new Date(item.createdAt * 1000), { addSuffix: true }); const primaryImage = item.images[0]; const isWTB = item.listingType === 'wtb'; // 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 // Skateboard rating for seller const sellerRating = getSkateboardRating(salesCount); // Trust level const trustLevel = getTrustLevel(trustScore); const trustBadge = SELLER_TRUST_BADGES[trustLevel]; const isSold = item.status === 'sold'; const handleMarkSold = async () => { try { await markSold(item.event); toast({ title: t('marketUi.card.markedSold'), description: t('marketUi.card.markedSoldBody') }); } catch (error) { toast({ title: t('marketUi.card.couldNotUpdate'), description: error instanceof Error ? error.message : t('marketUi.card.somethingWrong'), variant: 'destructive', }); } }; const handleRelist = async () => { try { await relist(item.event); toast({ title: t('marketUi.card.relisted'), description: t('marketUi.card.relistedBody') }); } catch (error) { toast({ title: t('marketUi.card.couldNotUpdate'), description: error instanceof Error ? error.message : t('marketUi.card.somethingWrong'), variant: 'destructive', }); } }; return ( navigate(`/listing/${item.pubkey}/${item.identifier}`)} > {/* Image */}
{primaryImage ? ( {item.title} ) : (
{isWTB ? '🔍' : '🛹'}
)} {/* Sold overlay — dims the image and stamps it clearly */} {isSold && (
{t('marketplace.sold')}
)} {/* WTB Badge */} {isWTB && ( {t('marketUi.gearSwap.wtb')} )} {/* Price Badge */}
{priceDisplay}
{/* Condition Badge (bottom left) */} {conditionInfo && ( {conditionInfo.emoji} {conditionInfo.label} )} {/* More options menu — owners manage their listing (mark sold / relist / delete), non-owners can report, admins can take down. */} {user && (
{/* Owner: mark sold / relist + delete */} {isOwnListingCheck && ( <> {isSold ? ( { e.stopPropagation(); handleRelist(); }} disabled={isUpdatingStatus} > {t('marketUi.listingDetail.relistActive')} ) : ( { e.stopPropagation(); handleMarkSold(); }} disabled={isUpdatingStatus} > {t('marketUi.listingDetail.markSold')} )} { e.stopPropagation(); setShowRemoveDialog(true); }} className="text-red-500 focus:text-red-500" > {t('marketUi.listingDetail.deleteListing')} )} {!isOwnListingCheck && ( { e.stopPropagation(); setShowReportDialog(true); }} className="text-red-500 focus:text-red-500" > {t('marketUi.card.report')} )} {isAdmin && !isOwnListingCheck && ( { e.stopPropagation(); setShowRemoveDialog(true); }} className="text-red-500 focus:text-red-500" > {t('marketUi.card.removeAdmin')} )}
)}
{/* Content */}
{/* Title */}

{item.title}

{/* Category + Size */}
{categoryLabel && ( {categoryLabel} )} {item.size && ( {item.size} )} {item.fulfilment && ( {item.fulfilment === 'pickup' ? `📍 ${t('marketUi.create.pickup')}` : item.fulfilment === 'ships' ? `📦 ${t('marketUi.create.ships')}` : `📍📦 ${t('marketUi.create.both')}`} )} {item.shipping && ( {t('marketUi.card.postage')}: {item.shipping} )} {distanceKm != null && ( {distanceKm < 1 ? `${Math.round(distanceKm * 1000)} m` : `${distanceKm.toFixed(1)} km`} )}
{/* Location */} {item.location && (
{item.location}
)} {/* Divider */}
{/* Seller Info */}
{displayName.slice(0, 2).toUpperCase()} {displayName} {/* Trust Badge */} {trustLevel === 'verified' && } {trustLevel === 'trusted' && } {trustLevel === 'new' && } {trustBadge.label}

{trustBadge.description}

{/* Skateboard Rating */}
{sellerRating.rating > 0 ? ( {sellerRating.display} ) : ( 🛹 {t('marketUi.card.newSeller')} )}

{salesCount === 1 ? t('marketUi.card.saleOne', { count: salesCount }) : t('marketUi.card.saleMany', { count: salesCount })}

{sellerRating.label}

{timeAgo}
{/* Contact Button */}
{/* Report Dialog */} {/* Admin takedown confirmation */} e.stopPropagation()} > {ownerDeleting ? t('marketUi.card.deleteTitle', { title: item.title }) : t('marketUi.card.adminRemoveTitle')} {ownerDeleting ? t('marketUi.card.deleteBody', { title: item.title }) : t('marketUi.card.adminRemoveBody', { title: item.title })} {t('common.cancel')} {isRemoving ? (ownerDeleting ? t('marketUi.card.deleting') : t('marketUi.card.removing')) : (ownerDeleting ? t('marketUi.card.deleteListing') : t('marketUi.card.removeListing'))}
); }