fork
Configure Feed
Select the types of activity you want to include in your feed.
👁️
fork
Configure Feed
Select the types of activity you want to include in your feed.
1/**
2 * Shared TanStack Query definitions
3 */
4
5import { queryOptions } from "@tanstack/react-query";
6import { getCardDataProvider } from "./card-data-provider";
7import type { OracleId, ScryfallId, VolatileData } from "./scryfall-types";
8import type {
9 Card,
10 PaginatedSearchResult,
11 SearchRestrictions,
12 SortOption,
13 UnifiedSearchResult,
14} from "./search-types";
15
16/**
17 * Combine function for useQueries - converts query results into a Map.
18 * Returns undefined until all cards are loaded.
19 */
20export function combineCardQueries(
21 results: Array<{ data?: Card | undefined }>,
22): Map<ScryfallId, Card> | undefined {
23 const map = new Map<ScryfallId, Card>();
24 for (const result of results) {
25 if (result.data) {
26 map.set(result.data.id, result.data);
27 }
28 }
29 return results.every((r) => r.data) ? map : undefined;
30}
31
32/**
33 * Search cards by name with optional restrictions
34 */
35export const searchCardsQueryOptions = (
36 query: string,
37 restrictions?: SearchRestrictions,
38 maxResults = 50,
39) =>
40 queryOptions({
41 queryKey: ["cards", "search", query, restrictions, maxResults] as const,
42 queryFn: async (): Promise<{ cards: Card[]; totalCount: number }> => {
43 const provider = await getCardDataProvider();
44
45 if (!query.trim()) {
46 // No search query - return empty results
47 return { cards: [], totalCount: 0 };
48 }
49
50 // Search may not be available on all providers (e.g., server-side)
51 if (!provider.searchCards) {
52 return { cards: [], totalCount: 0 };
53 }
54
55 const cards = await provider.searchCards(query, restrictions, maxResults);
56 const metadata = await provider.getMetadata();
57
58 return {
59 cards,
60 totalCount: metadata.cardCount,
61 };
62 },
63 staleTime: 5 * 60 * 1000, // 5 minutes
64 });
65
66/**
67 * Get a single card by ID
68 */
69export const getCardByIdQueryOptions = (id: ScryfallId) =>
70 queryOptions({
71 queryKey: ["cards", "byId", id] as const,
72 queryFn: async () => {
73 const provider = await getCardDataProvider();
74 return provider.getCardById(id);
75 },
76 staleTime: Number.POSITIVE_INFINITY,
77 });
78
79/**
80 * Get all printings for a card's oracle ID
81 */
82export const getCardPrintingsQueryOptions = (oracleId: OracleId) =>
83 queryOptions({
84 queryKey: ["cards", "printings", oracleId] as const,
85 queryFn: async (): Promise<ScryfallId[]> => {
86 const provider = await getCardDataProvider();
87 return provider.getPrintingsByOracleId(oracleId);
88 },
89 staleTime: Number.POSITIVE_INFINITY,
90 });
91
92/**
93 * Get cards metadata (version, count)
94 */
95export const getCardsMetadataQueryOptions = () =>
96 queryOptions({
97 queryKey: ["cards", "metadata"] as const,
98 queryFn: async () => {
99 const provider = await getCardDataProvider();
100 return provider.getMetadata();
101 },
102 staleTime: Number.POSITIVE_INFINITY,
103 });
104
105/**
106 * Get canonical printing ID for an oracle ID
107 */
108export const getCanonicalPrintingQueryOptions = (oracleId: OracleId) =>
109 queryOptions({
110 queryKey: ["cards", "canonical", oracleId] as const,
111 queryFn: async (): Promise<ScryfallId | null> => {
112 const provider = await getCardDataProvider();
113 const result = await provider.getCanonicalPrinting(oracleId);
114 return result ?? null;
115 },
116 staleTime: Number.POSITIVE_INFINITY,
117 });
118
119export type SyntaxSearchResult =
120 | { ok: true; cards: Card[] }
121 | { ok: false; error: { message: string; start: number; end: number } };
122
123/**
124 * Search cards using Scryfall-like syntax (e.g., "t:creature cmc<=3 s:lea")
125 */
126export const syntaxSearchQueryOptions = (query: string, maxResults = 100) =>
127 queryOptions({
128 queryKey: ["cards", "syntaxSearch", query, maxResults] as const,
129 queryFn: async (): Promise<SyntaxSearchResult> => {
130 const provider = await getCardDataProvider();
131
132 if (!query.trim()) {
133 return { ok: true, cards: [] };
134 }
135
136 if (!provider.syntaxSearch) {
137 return {
138 ok: false,
139 error: { message: "Syntax search not available", start: 0, end: 0 },
140 };
141 }
142
143 return provider.syntaxSearch(query, maxResults);
144 },
145 staleTime: 5 * 60 * 1000,
146 });
147
148/**
149 * Get volatile data (prices, EDHREC rank) for a card
150 */
151export const getVolatileDataQueryOptions = (id: ScryfallId) =>
152 queryOptions({
153 queryKey: ["cards", "volatile", id] as const,
154 queryFn: async (): Promise<VolatileData | null> => {
155 const provider = await getCardDataProvider();
156 return provider.getVolatileData(id);
157 },
158 staleTime: 5 * 60 * 1000, // 5 minutes - prices change frequently
159 });
160
161export type { UnifiedSearchResult };
162
163/**
164 * Unified search that automatically routes to fuzzy or syntax search
165 * based on query complexity. Returns mode indicator and description for syntax queries.
166 */
167export const unifiedSearchQueryOptions = (
168 query: string,
169 restrictions?: SearchRestrictions,
170 maxResults = 50,
171) =>
172 queryOptions({
173 queryKey: [
174 "cards",
175 "unifiedSearch",
176 query,
177 restrictions,
178 maxResults,
179 ] as const,
180 queryFn: async (): Promise<UnifiedSearchResult> => {
181 const provider = await getCardDataProvider();
182
183 if (!query.trim()) {
184 return { mode: "fuzzy", cards: [], description: null, error: null };
185 }
186
187 if (!provider.unifiedSearch) {
188 return {
189 mode: "fuzzy",
190 cards: [],
191 description: null,
192 error: { message: "Unified search not available", start: 0, end: 0 },
193 };
194 }
195
196 return provider.unifiedSearch(query, restrictions, maxResults);
197 },
198 staleTime: 5 * 60 * 1000,
199 });
200
201export const PAGE_SIZE = 50;
202
203/**
204 * Single page query for visibility-based fetching.
205 * Used with useQueries to load pages based on scroll position.
206 */
207export const searchPageQueryOptions = (
208 query: string,
209 offset: number,
210 restrictions?: SearchRestrictions,
211 sort: SortOption[] = [{ field: "name", direction: "auto" }],
212) =>
213 queryOptions({
214 queryKey: [
215 "cards",
216 "searchPage",
217 query,
218 offset,
219 restrictions,
220 sort,
221 ] as const,
222 queryFn: async (): Promise<PaginatedSearchResult> => {
223 const provider = await getCardDataProvider();
224
225 if (!query.trim()) {
226 return {
227 mode: "fuzzy",
228 cards: [],
229 totalCount: 0,
230 description: null,
231 error: null,
232 };
233 }
234
235 if (!provider.paginatedUnifiedSearch) {
236 return {
237 mode: "fuzzy",
238 cards: [],
239 totalCount: 0,
240 description: null,
241 error: {
242 message: "Paginated search not available",
243 start: 0,
244 end: 0,
245 },
246 };
247 }
248
249 return provider.paginatedUnifiedSearch(
250 query,
251 restrictions,
252 sort,
253 offset,
254 PAGE_SIZE,
255 );
256 },
257 staleTime: 5 * 60 * 1000,
258 });