👁️
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

use infinite query for paginated listings

+68 -30
+6 -5
src/components/list/SaveToListButton.tsx
··· 1 - import { useQuery } from "@tanstack/react-query"; 1 + import { useInfiniteQuery } from "@tanstack/react-query"; 2 2 import { Bookmark } from "lucide-react"; 3 3 import { useMemo, useState } from "react"; 4 4 import { listUserCollectionListsQueryOptions } from "@/lib/collection-list-queries"; ··· 20 20 const { session } = useAuth(); 21 21 const [isOpen, setIsOpen] = useState(false); 22 22 23 - const { data: listsData } = useQuery({ 23 + const { data: listsData } = useInfiniteQuery({ 24 24 ...listUserCollectionListsQueryOptions(session?.info.sub ?? ("" as never)), 25 25 enabled: !!session, 26 26 }); 27 27 28 + const lists = listsData?.pages.flatMap((p) => p.records) ?? []; 29 + 28 30 const isSaved = useMemo(() => { 29 - if (!listsData?.records) return false; 30 - return listsData.records.some((record) => 31 + return lists.some((record) => 31 32 item.type === "card" 32 33 ? hasCard(record.value, item.scryfallId) 33 34 : hasDeck(record.value, item.deckUri), 34 35 ); 35 - }, [listsData, item]); 36 + }, [lists, item]); 36 37 37 38 if (!session) { 38 39 return null;
+3 -3
src/components/list/SaveToListDialog.tsx
··· 1 1 import type { Did } from "@atcute/lexicons"; 2 - import { useQuery } from "@tanstack/react-query"; 2 + import { useInfiniteQuery } from "@tanstack/react-query"; 3 3 import { Bookmark, Loader2, Plus } from "lucide-react"; 4 4 import { useEffect, useId, useState } from "react"; 5 5 import { toast } from "sonner"; ··· 41 41 const inputId = useId(); 42 42 const [newListName, setNewListName] = useState(""); 43 43 44 - const { data: listsData, isLoading } = useQuery({ 44 + const { data: listsData, isLoading } = useInfiniteQuery({ 45 45 ...listUserCollectionListsQueryOptions(userDid), 46 46 enabled: isOpen, 47 47 }); ··· 83 83 ); 84 84 }; 85 85 86 - const lists = listsData?.records ?? []; 86 + const lists = listsData?.pages.flatMap((p) => p.records) ?? []; 87 87 88 88 return ( 89 89 <>
+12 -2
src/lib/atproto-client.ts
··· 211 211 collection: Collection, 212 212 entityName: string, 213 213 schema: TSchema, 214 + cursor?: string, 214 215 ): Promise<Result<ListRecordsResponse<InferOutput<TSchema>>>> { 215 216 try { 216 217 const url = new URL(`${pdsUrl}/xrpc/com.atproto.repo.listRecords`); 217 218 url.searchParams.set("repo", did); 218 219 url.searchParams.set("collection", collection); 220 + if (cursor) { 221 + url.searchParams.set("cursor", cursor); 222 + } 219 223 220 224 const response = await fetch(url.toString()); 221 225 ··· 330 334 return updateRecord(agent, rkey, record, DECK_COLLECTION, "deck"); 331 335 } 332 336 333 - export function listUserDecks(pdsUrl: PdsUrl, did: Did) { 337 + export function listUserDecks(pdsUrl: PdsUrl, did: Did, cursor?: string) { 334 338 return listRecords( 335 339 pdsUrl, 336 340 did, 337 341 DECK_COLLECTION, 338 342 "deck", 339 343 ComDeckbelcherDeckList.mainSchema, 344 + cursor, 340 345 ); 341 346 } 342 347 ··· 376 381 return updateRecord(agent, rkey, record, LIST_COLLECTION, "list"); 377 382 } 378 383 379 - export function listUserCollectionLists(pdsUrl: PdsUrl, did: Did) { 384 + export function listUserCollectionLists( 385 + pdsUrl: PdsUrl, 386 + did: Did, 387 + cursor?: string, 388 + ) { 380 389 return listRecords( 381 390 pdsUrl, 382 391 did, 383 392 LIST_COLLECTION, 384 393 "list", 385 394 ComDeckbelcherCollectionList.mainSchema, 395 + cursor, 386 396 ); 387 397 } 388 398
+17 -4
src/lib/collection-list-queries.ts
··· 4 4 */ 5 5 6 6 import type { Did } from "@atcute/lexicons"; 7 - import { queryOptions, useQueryClient } from "@tanstack/react-query"; 7 + import { 8 + infiniteQueryOptions, 9 + queryOptions, 10 + useQueryClient, 11 + } from "@tanstack/react-query"; 8 12 import { useNavigate } from "@tanstack/react-router"; 9 13 import { toast } from "sonner"; 10 14 import { ··· 99 103 * Query options for listing all collection lists for a user 100 104 */ 101 105 export const listUserCollectionListsQueryOptions = (did: Did) => 102 - queryOptions({ 106 + infiniteQueryOptions({ 103 107 queryKey: ["collection-lists", did] as const, 104 - queryFn: async (): Promise<{ records: CollectionListRecord[] }> => { 108 + queryFn: async ({ 109 + pageParam, 110 + }): Promise<{ records: CollectionListRecord[]; cursor?: string }> => { 105 111 const pds = await getPdsForDid(did); 106 - const result = await listUserCollectionLists(asPdsUrl(pds), did); 112 + const result = await listUserCollectionLists( 113 + asPdsUrl(pds), 114 + did, 115 + pageParam, 116 + ); 107 117 if (!result.success) { 108 118 throw result.error; 109 119 } ··· 113 123 cid: record.cid, 114 124 value: transformListRecord(record.value), 115 125 })), 126 + cursor: result.data.cursor, 116 127 }; 117 128 }, 129 + initialPageParam: undefined as string | undefined, 130 + getNextPageParam: (lastPage) => lastPage.cursor, 118 131 staleTime: 60 * 1000, 119 132 }); 120 133
+13 -4
src/lib/deck-queries.ts
··· 4 4 */ 5 5 6 6 import type { Did } from "@atcute/lexicons"; 7 - import { queryOptions, useQueryClient } from "@tanstack/react-query"; 7 + import { 8 + infiniteQueryOptions, 9 + queryOptions, 10 + useQueryClient, 11 + } from "@tanstack/react-query"; 8 12 import { useNavigate } from "@tanstack/react-router"; 9 13 import { toast } from "sonner"; 10 14 import { ··· 79 83 * Fetches from user's PDS directly 80 84 */ 81 85 export const listUserDecksQueryOptions = (did: Did) => 82 - queryOptions({ 86 + infiniteQueryOptions({ 83 87 queryKey: ["decks", did] as const, 84 - queryFn: async (): Promise<{ records: DeckListRecord[] }> => { 88 + queryFn: async ({ 89 + pageParam, 90 + }): Promise<{ records: DeckListRecord[]; cursor?: string }> => { 85 91 const pds = await getPdsForDid(did); 86 - const result = await listUserDecks(asPdsUrl(pds), did); 92 + const result = await listUserDecks(asPdsUrl(pds), did, pageParam); 87 93 if (!result.success) { 88 94 throw result.error; 89 95 } ··· 93 99 cid: record.cid, 94 100 value: transformDeckRecord(record.value), 95 101 })), 102 + cursor: result.data.cursor, 96 103 }; 97 104 }, 105 + initialPageParam: undefined as string | undefined, 106 + getNextPageParam: (lastPage) => lastPage.cursor, 98 107 staleTime: 60 * 1000, // 1 minute 99 108 }); 100 109
+17 -12
src/routes/profile/$did/index.tsx
··· 1 1 import type { Did } from "@atcute/lexicons"; 2 - import { useQuery, useSuspenseQuery } from "@tanstack/react-query"; 2 + import { 3 + useInfiniteQuery, 4 + useQuery, 5 + useSuspenseInfiniteQuery, 6 + } from "@tanstack/react-query"; 3 7 import { createFileRoute, Link } from "@tanstack/react-router"; 4 8 import { Plus } from "lucide-react"; 5 9 import { useMemo } from "react"; ··· 33 37 loader: async ({ context, params }) => { 34 38 // Prefetch deck list, collection lists, and DID document during SSR 35 39 const [, , didDocument] = await Promise.all([ 36 - context.queryClient.ensureQueryData( 40 + context.queryClient.ensureInfiniteQueryData( 37 41 listUserDecksQueryOptions(params.did as Did), 38 42 ), 39 - context.queryClient.ensureQueryData( 43 + context.queryClient.ensureInfiniteQueryData( 40 44 listUserCollectionListsQueryOptions(params.did as Did), 41 45 ), 42 46 context.queryClient.ensureQueryData( ··· 95 99 const search = Route.useSearch(); 96 100 const navigate = Route.useNavigate(); 97 101 const { session } = useAuth(); 98 - const { data: decksData } = useSuspenseQuery( 102 + const { data: decksData } = useSuspenseInfiniteQuery( 99 103 listUserDecksQueryOptions(did as Did), 100 104 ); 101 - const { data: listsData } = useQuery( 105 + const { data: listsData } = useInfiniteQuery( 102 106 listUserCollectionListsQueryOptions(did as Did), 103 107 ); 104 108 const { data: didDocument } = useQuery(didDocumentQueryOptions(did as Did)); ··· 106 110 107 111 const handle = extractHandle(didDocument ?? null); 108 112 const isOwner = session?.info.sub === did; 109 - const lists = listsData?.records ?? []; 113 + const decks = decksData.pages.flatMap((p) => p.records); 114 + const lists = listsData?.pages.flatMap((p) => p.records) ?? []; 110 115 111 116 // Get unique formats for filter dropdown 112 117 const availableFormats = useMemo(() => { 113 118 const formats = new Set<string>(); 114 - for (const record of decksData.records) { 119 + for (const record of decks) { 115 120 if (record.value.format) { 116 121 formats.add(record.value.format); 117 122 } 118 123 } 119 124 return Array.from(formats).sort(); 120 - }, [decksData.records]); 125 + }, [decks]); 121 126 122 127 // Filter and sort 123 128 const filteredAndSorted = useMemo(() => { 124 - let records = decksData.records; 129 + let records = decks; 125 130 126 131 // Filter by format 127 132 if (search.format) { ··· 130 135 131 136 // Sort 132 137 return sortDecks(records, search.sort); 133 - }, [decksData.records, search.format, search.sort]); 138 + }, [decks, search.format, search.sort]); 134 139 135 140 const handleSortChange = (e: React.ChangeEvent<HTMLSelectElement>) => { 136 141 const value = e.target.value as SortOption | ""; ··· 169 174 </div> 170 175 171 176 {/* Sort and filter controls - only show if there are decks */} 172 - {decksData.records.length > 0 && ( 177 + {decks.length > 0 && ( 173 178 <div className="flex flex-wrap gap-4 mb-6"> 174 179 <select 175 180 value={search.sort ?? "updated-desc"} ··· 205 210 <h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4"> 206 211 Decks 207 212 </h2> 208 - {decksData.records.length === 0 ? ( 213 + {decks.length === 0 ? ( 209 214 <div className="text-center py-8 bg-gray-50 dark:bg-slate-800 rounded-lg"> 210 215 <p className="text-gray-600 dark:text-gray-400 mb-4"> 211 216 {isOwner ? "No decklists yet" : "No decklists"}