import { dateLocaleCode } from '@/lib/dateFnsI18n'; import { useState } from 'react'; import { useParams, Link, useNavigate } from 'react-router-dom'; import { useSeoMeta } from '@unhead/react'; import { nip19 } from 'nostr-tools'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Skeleton } from '@/components/ui/skeleton'; import { ImageWithFallback } from '@/components/ImageWithFallback'; import { ImageLightbox, type LightboxImage } from '@/components/ImageLightbox'; import { TrustBadge } from '@/components/TrustBadge'; import { TrustFlaggedNotice } from '@/components/TrustFlaggedNotice'; import { TradeReputationCard } from '@/components/TradeReputationCard'; import { ListingTradeActions } from '@/components/ListingTradeActions'; import { ListingOffersPanel } from '@/components/ListingOffersPanel'; import { ClinkPayButton } from '@/components/ClinkPayButton'; import { OfferDialog } from '@/components/OfferDialog'; import { MarkSoldDialog } from '@/components/MarkSoldDialog'; import { DeleteEntityDialog } from '@/components/DeleteEntityDialog'; import { useMarketplaceItem } from '@/hooks/useMarketplace'; import { useMarkListingSold } from '@/hooks/useMarkListingSold'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useAuthor } from '@/hooks/useAuthor'; import { useSellerInfo } from '@/hooks/useSellerInfo'; import { useToast } from '@/hooks/useToast'; import { ErrorState } from '@/components/ErrorState'; import { formatPrice, GEAR_CONDITIONS, GEAR_CATEGORIES, isValidNoffer } from '@/lib/types'; import { GEAR_CATEGORY_KEYS, GEAR_CONDITION_KEYS } from '@/lib/labelKeys'; import { genUserName } from '@/lib/genUserName'; import { cn } from '@/lib/utils'; import { ArrowLeft, CheckCircle, RotateCcw, Trash2, Package, MapPin, Calendar, Tag, Handshake, Share2, } from 'lucide-react'; import { useTranslation } from '@/contexts/language'; /** * Full marketplace listing page (`/listing/:pubkey/:identifier`). * * Gives a listing a dedicated, shareable surface: a large gallery, the full * description, seller trust + trade reputation, and the shared trade/escrow * actions (`ListingTradeActions`, also used by the grid card). */ export default function ListingDetailPage() { const { t } = useTranslation(); const { pubkey, identifier } = useParams<{ pubkey: string; identifier: string }>(); const navigate = useNavigate(); const { user } = useCurrentUser(); const { toast } = useToast(); const { data: item, isLoading, isError, refetch } = useMarketplaceItem(pubkey, identifier); const author = useAuthor(pubkey); const metadata = author.data?.metadata; const { data: sellerInfo } = useSellerInfo(pubkey); const { relist, isPending: isUpdatingStatus } = useMarkListingSold(); const [lightboxIndex, setLightboxIndex] = useState(null); const [showMarkSold, setShowMarkSold] = useState(false); const [showDelete, setShowDelete] = useState(false); const [showOffer, setShowOffer] = useState(false); useSeoMeta({ title: item ? `${item.title} — SkateSpots Marketplace` : 'Listing — SkateSpots', description: item?.summary || item?.description || 'A listing on the SkateSpots marketplace.', }); if (isLoading) { return ( <>
); } if (isError) { return (
{ void refetch(); }} />
); } if (!item) { return ( <>

{t('marketUi.listingDetail.notFound')}

{t('marketUi.listingDetail.notFoundBody')}

); } const isOwnListing = user?.pubkey === item.pubkey; const isWTB = item.listingType === 'wtb'; const isSold = item.status === 'sold'; const priceDisplay = item.price ? `${formatPrice(item.price, item.priceType, t)}${item.price.frequency ? `/${item.price.frequency}` : ''}` : formatPrice(undefined, item.priceType, t); const conditionInfo = item.condition ? GEAR_CONDITIONS[item.condition] : null; const categoryValue = item.category.find((c) => GEAR_CATEGORIES[c as keyof typeof GEAR_CATEGORIES]); const categoryLabel = categoryValue ? GEAR_CATEGORY_KEYS[categoryValue] ? t(GEAR_CATEGORY_KEYS[categoryValue]) : GEAR_CATEGORIES[categoryValue as keyof typeof GEAR_CATEGORIES] : null; const displayName = metadata?.name ?? (pubkey ? genUserName(pubkey) : t('marketUi.listingDetail.unknown')); const npub = pubkey ? nip19.npubEncode(pubkey) : ''; const sellerNoffer = typeof (metadata as Record | undefined)?.noffer === 'string' ? ((metadata as Record).noffer as string) : undefined; const lightboxImages: LightboxImage[] = item.images.map((url) => ({ url, isOwner: isOwnListing })); const handleShare = async () => { try { await navigator.clipboard.writeText(window.location.href); toast({ title: t('marketUi.listingDetail.linkCopied'), description: t('marketUi.listingDetail.linkCopiedBody') }); } catch { toast({ title: t('marketUi.listingDetail.copyFailed'), variant: 'destructive' }); } }; const handleRelist = async () => { try { await relist(item.event); toast({ title: t('marketUi.listingDetail.relisted'), description: t('marketUi.listingDetail.relistedBody') }); } catch (error) { toast({ title: t('marketUi.listingDetail.couldNotUpdate'), description: error instanceof Error ? error.message : t('marketUi.listingDetail.somethingWrong'), variant: 'destructive', }); } }; return ( <>
{/* Main column */}
{/* Gallery */} {item.images.length > 0 && (
{item.images.map((url, i) => ( ))}
)} {/* Title + price */}
{isWTB && ( {t('marketUi.listingDetail.wantToBuy')} )} {isSold && ( {t('marketUi.listingDetail.sold')} )} {item.status && item.status !== 'active' && !isSold && ( {item.status === 'pending' ? t('marketUi.statusPending') : item.status.toUpperCase()} )}

{item.title}

{priceDisplay}

{/* Meta badges */}
{conditionInfo && ( {conditionInfo.emoji} {item.condition && GEAR_CONDITION_KEYS[item.condition] ? t(GEAR_CONDITION_KEYS[item.condition]) : conditionInfo.label} )} {item.size && ( {item.size} )} {categoryLabel && ( {categoryLabel} )}
{/* Description */} {item.description && ( {t('marketUi.listingDetail.description')}

{item.description}

)} {/* Details */} {item.location && (
{item.location}
)} {item.availability && (
{t('marketUi.listingDetail.usuallyHere', { availability: item.availability })}
)}
{' '} {t('marketUi.listingDetail.listed', { date: new Date(item.createdAt * 1000).toLocaleDateString(dateLocaleCode()) })}
{/* Sidebar */}
setLightboxIndex(null)} /> {!isOwnListing && ( )} {isOwnListing && ( )} {isOwnListing && ( )} ); }