import { useParams, Link, useNavigate } from 'react-router-dom'; import { useSeoMeta } from '@unhead/react'; import { useNostr } from '@nostrify/react'; import { useQuery } from '@tanstack/react-query'; import { PageError } from '@/components/ErrorState'; import { SpotCard, SpotCardSkeleton } from '@/components/SpotCard'; import { SpotMap } from '@/components/SpotMap'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Skeleton } from '@/components/ui/skeleton'; import { Badge } from '@/components/ui/badge'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog'; import { useToast } from '@/hooks/useToast'; import { useTranslation } from '@/contexts/language'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useRelays } from '@/lib/relays'; import { useSpotCollection, useRemoveSpotFromCollection, useDeleteSpotCollection } from '@/hooks/useSpotCollections'; import type { NostrEvent } from '@nostrify/nostrify'; import { SKATE_SPOT_KIND, parseSkateSpot, type SkateSpot } from '@/lib/types'; import { ArrowLeft, FolderOpen, MapPin, Calendar, Trash2, X, Share2, Loader2, AlertTriangle } from 'lucide-react'; import NotFound from './NotFound'; import { useState } from 'react'; function CollectionDetailSkeleton() { return ( <>
{Array.from({ length: 6 }).map((_, i) => ( ))}
); } export default function CollectionDetailPage() { const { t } = useTranslation(); const { pubkey, identifier } = useParams<{ pubkey: string; identifier: string }>(); const { data: collection, isLoading, error, refetch } = useSpotCollection(identifier || '', pubkey); const { user } = useCurrentUser(); const { nostr } = useNostr(); const { read } = useRelays(); const { removeSpotFromCollection, isPending: isRemoving } = useRemoveSpotFromCollection(); const { deleteCollection, isPending: isDeleting } = useDeleteSpotCollection(); const { toast } = useToast(); const navigate = useNavigate(); const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [removingSpotId, setRemovingSpotId] = useState(null); const isOwner = user && collection && user.pubkey === collection.pubkey; useSeoMeta({ title: collection ? `${collection.title} - SkateSpots Collection` : 'Spot Collection - SkateSpots', description: collection?.description || 'A curated collection of skate spots', }); // Fetch all spots referenced in the collection const { data: spots, isLoading: isSpotsLoading } = useQuery({ queryKey: ['collection-spots', collection?.id, collection?.spotRefs], queryFn: async (c) => { if (!collection || collection.spotRefs.length === 0) return []; const signal = AbortSignal.any([c.signal, AbortSignal.timeout(15000)]); const readRelays = read; // Parse spot references to build filters const spotFilters = collection.spotRefs.map((ref) => { const [kind, pubkey, d] = ref.split(':'); return { kind: parseInt(kind), pubkey, d }; }).filter(f => f.kind === SKATE_SPOT_KIND); if (spotFilters.length === 0) return []; const allEvents: NostrEvent[] = []; // Query each relay for all spots for (const relayUrl of readRelays) { try { const relay = nostr.relay(relayUrl); // Query each spot individually (addressable events need specific filters) for (const filter of spotFilters) { const events = await relay.query( [{ kinds: [SKATE_SPOT_KIND], authors: [filter.pubkey], '#d': [filter.d], limit: 1, }], { signal } ); allEvents.push(...events); } } catch (error) { console.warn(`[CollectionDetail] Failed to query relay ${relayUrl}:`, error); } } // Deduplicate by event id const eventMap = new Map(); for (const event of allEvents) { const d = event.tags.find(([name]: [string]) => name === 'd')?.[1]; if (d) { const key = `${event.pubkey}:${event.kind}:${d}`; const existing = eventMap.get(key); if (!existing || event.created_at > existing.created_at) { eventMap.set(key, event); } } } const spots: SkateSpot[] = []; for (const event of Array.from(eventMap.values())) { const spot = parseSkateSpot(event); if (spot) spots.push(spot); } return spots; }, enabled: !!collection && collection.spotRefs.length > 0, staleTime: 30000, }); const handleRemoveSpot = async (spot: SkateSpot) => { if (!collection || !isOwner) return; setRemovingSpotId(spot.id); try { await removeSpotFromCollection({ collection, spotPubkey: spot.pubkey, spotIdentifier: spot.identifier, }); toast({ title: t('walletUi.collection.spotRemoved'), description: t('walletUi.collection.spotRemovedDesc', { title: spot.title }), }); } catch (error) { toast({ title: t('walletUi.collection.removeFailed'), description: error instanceof Error ? error.message : t('walletUi.addToCollection.somethingWrong'), variant: 'destructive', }); } finally { setRemovingSpotId(null); } }; const handleDeleteCollection = async () => { if (!collection) return; try { await deleteCollection(collection); toast({ title: t('walletUi.collection.deleted'), description: t('walletUi.collection.deletedDesc', { title: collection.title }), }); navigate('/collections'); } catch (error) { toast({ title: t('walletUi.collection.deleteFailed'), description: error instanceof Error ? error.message : t('walletUi.addToCollection.somethingWrong'), variant: 'destructive', }); } setShowDeleteDialog(false); }; const handleShare = async () => { if (navigator.share) { try { await navigator.share({ title: collection?.title, text: collection?.description || t('walletUi.collection.shareText', { title: collection?.title ?? '' }), url: window.location.href, }); } catch { // User cancelled or share failed } } else { navigator.clipboard.writeText(window.location.href); toast({ title: t('walletUi.collection.linkCopied'), description: t('walletUi.collection.linkCopiedDesc'), }); } }; if (isLoading) { return ; } if (error) { return refetch()} />; } if (!collection) { return ; } return ( <>
{/* Back link */} {t('walletUi.collection.back')} {/* Header */}

{collection.title}

{collection.description && (

{collection.description}

)}
{isOwner && ( )}
{t('walletUi.addToCollection.spotCount', { count: collection.spotRefs.length })} {t('walletUi.collection.created', { date: new Date(collection.createdAt * 1000).toLocaleDateString() })}
{/* Map showing all spots */} {spots && spots.length > 0 && ( {t('walletUi.collection.map')}
)} {/* Spots grid */} {isSpotsLoading ? (
{Array.from({ length: collection.spotRefs.length || 3 }).map((_, i) => ( ))}
) : spots && spots.length > 0 ? (
{spots.map((spot) => (
{isOwner && ( )}
))}
) : (

{t('walletUi.collection.empty')}

{isOwner ? t('walletUi.collection.emptyOwner') : t('walletUi.collection.emptyOther')}

{isOwner && ( )}
)}
{/* Delete Confirmation Dialog */} {t('walletUi.collection.deleteTitle')} {t('walletUi.collection.deleteDesc', { title: collection.title })} {t('common.cancel')} {isDeleting ? ( <> {t('walletUi.collection.deleting')} ) : ( t('common.delete') )} ); }