- {/* NOTE: there is deliberately no "Pay with Cashu" button here.
- Payments are not implemented, and a button that only shows a
- "coming soon" toast is a dead end — buyers contact the seller
- and arrange payment directly. */}
-
- {/* Buyer: confirm the trade the seller recorded (builds both reps) */}
- {sellerNamedMe && !iConfirmedTrade && (
-
- )}
-
- {/* Buyer: lock funds in Cashu escrow and request confirmation (native) */}
- {canEscrow && (
-
- )}
-
- {/* Buyer: ask the seller to record the sale (kind 31872 request) */}
- {!canEscrow && user && !isOwnListing && isSold && !iConfirmedTrade && !sellerNamedMe && !iRequestedConfirmation && (
-
- )}
- {iRequestedConfirmation && !iConfirmedTrade && (
-
- Confirmation requested — waiting on the seller.
-
- )}
-
- {/* Buyer: reclaim escrow after the locktime if the seller didn't deliver */}
- {escrow.available && myEscrowRefundable && (
-
- )}
-
- {/* Contact button */}
-
-
+
@@ -620,16 +441,6 @@ export function MarketplaceItemCard({ item, compact = false }: MarketplaceItemCa
/>
)}
- {/* Buyer: Cashu escrow + confirmation request (native-only) */}
- {canEscrow && sellerEscrowPubkey && (
-
- )}
{/* Delete confirmation (controlled — trigger lives in the owner dropdown) */}
=> {
+ if (!pubkey || !identifier) return null;
+ const signal = AbortSignal.timeout(TIMEOUT);
+ const events = await querySkateContent(
+ [{ kinds: [MARKETPLACE_ITEM_KIND], authors: [pubkey], '#d': [identifier], limit: 5 }],
+ { signal },
+ );
+ const newest = [...events].sort((a, b) => b.created_at - a.created_at)[0];
+ return newest ? parseMarketplaceItem(newest) : null;
+ },
+ });
+}
diff --git a/src/pages/ListingDetailPage.tsx b/src/pages/ListingDetailPage.tsx
new file mode 100644
index 0000000..212762f
--- /dev/null
+++ b/src/pages/ListingDetailPage.tsx
@@ -0,0 +1,355 @@
+import { useState } from 'react';
+import { useParams, Link, useNavigate } from 'react-router-dom';
+import { useSeoMeta } from '@unhead/react';
+import { nip19 } from 'nostr-tools';
+import { Layout } from '@/components/Layout';
+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 { TradeReputationCard } from '@/components/TradeReputationCard';
+import { ListingTradeActions } from '@/components/ListingTradeActions';
+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 { formatPrice, GEAR_CONDITIONS, GEAR_CATEGORIES } from '@/lib/types';
+import { genUserName } from '@/lib/genUserName';
+import { cn } from '@/lib/utils';
+import {
+ ArrowLeft,
+ MessageCircle,
+ CheckCircle,
+ RotateCcw,
+ Trash2,
+ Package,
+ MapPin,
+ Calendar,
+ Tag,
+} from 'lucide-react';
+
+/**
+ * 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 { pubkey, identifier } = useParams<{ pubkey: string; identifier: string }>();
+ const navigate = useNavigate();
+ const { user } = useCurrentUser();
+ const { toast } = useToast();
+
+ const { data: item, isLoading } = 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);
+
+ useSeoMeta({
+ title: item ? `${item.title} — SkateSpots Marketplace` : 'Listing — SkateSpots',
+ description: item?.summary || item?.description || 'A listing on the SkateSpots marketplace.',
+ });
+
+ if (isLoading) {
+ return (
+
+