import { useEffect, useState } from "react"; import { api, CardSuggestion } from "./api"; import { CardThumb, ColorPips, DialogFrame } from "./components"; // Matches the backend's hardcoded per-page limit (src-tauri/src/commands.rs). // A batch shorter than this means there's nothing left to fetch. const PAGE_SIZE = 50; // Fixed display order, matching the four synergy families the backend scores. const CATEGORY_ORDER = ["Tribal", "Mechanic", "Archetype", "Mana base"]; function categorySlug(category: string): string { return category.toLowerCase().replace(/[^a-z]+/g, "-"); } /** * On-demand modal listing scored card suggestions for a deck, each with an * explainable synergy breakdown and "Add to deck"/"Add to maybeboard" * actions. Fetches a page on open; toggleable chips filter which reason * categories must be present (client-side, over everything loaded so far); * "Suggest more" fetches and appends the next page. Adding a card removes * its row in place so several can be added in one sitting. An "Unusual * picks" toggle re-fetches from the backend restricted to (and reordered * toward) cards that aren't widely played, surfacing underappreciated * synergy hits instead of the usual staples — that reordering only makes * sense server-side, before pagination slices the result, so toggling it * resets and re-fetches rather than filtering what's already loaded. */ export function SuggestionsModal({ deckId, onClose, onCardAdded, }: { deckId: number; onClose: () => void; onCardAdded: () => void; }) { const [suggestions, setSuggestions] = useState(null); const [error, setError] = useState(null); const [busy, setBusy] = useState>(new Set()); const [hasMore, setHasMore] = useState(true); const [loadingMore, setLoadingMore] = useState(false); const [activeCategories, setActiveCategories] = useState>(new Set()); const [unusual, setUnusual] = useState(false); useEffect(() => { let cancelled = false; setSuggestions(null); setError(null); api .suggestCards(deckId, 0, unusual) .then((s) => { if (cancelled) return; setSuggestions(s); setHasMore(s.length === PAGE_SIZE); }) .catch((e) => { if (!cancelled) setError(String(e)); }); return () => { cancelled = true; }; }, [deckId, unusual]); async function loadMore() { if (!suggestions) return; setLoadingMore(true); try { const more = await api.suggestCards(deckId, suggestions.length, unusual); setSuggestions((cur) => { const seen = new Set((cur ?? []).map((s) => s.card.oracle_id)); return [...(cur ?? []), ...more.filter((s) => !seen.has(s.card.oracle_id))]; }); setHasMore(more.length === PAGE_SIZE); } finally { setLoadingMore(false); } } async function add(s: CardSuggestion, maybeboard: boolean) { const oid = s.card.oracle_id; setBusy((b) => new Set(b).add(oid)); try { await api.setDeckCard(deckId, oid, 1, false, maybeboard); onCardAdded(); setSuggestions((cur) => (cur ? cur.filter((x) => x.card.oracle_id !== oid) : cur)); } finally { setBusy((b) => { const next = new Set(b); next.delete(oid); return next; }); } } function toggleCategory(cat: string) { setActiveCategories((cur) => { const next = new Set(cur); if (next.has(cat)) next.delete(cat); else next.add(cat); return next; }); } const availableCategories = suggestions ? CATEGORY_ORDER.filter((cat) => suggestions.some((s) => s.reasons.some((r) => r.category === cat))) : []; const visible = suggestions && activeCategories.size > 0 ? suggestions.filter((s) => s.reasons.some((r) => activeCategories.has(r.category))) : suggestions; return (
{error ? (

{error}

) : suggestions === null ? (

Analyzing your deck…

) : suggestions.length === 0 ? (

{unusual ? "No unusual picks found — try turning off \"Unusual picks\" to see the widely-played suggestions too." : "No suggestions yet — add a few cards (and a commander, if it's a Commander deck) so there's a strategy to build on."}

) : ( <> {availableCategories.length > 1 && (
Filter by why: {availableCategories.map((cat) => ( ))} {activeCategories.size > 0 && ( )}
)} {visible && visible.length === 0 ? (

No suggestions match the selected filters.

) : (
    {visible?.map((s) => (
  • {s.card.name} {s.quantity_owned > 0 ? ( Owned ×{s.quantity_owned} ) : ( Not owned )} {s.edhrec_rank !== null && s.edhrec_rank < 2000 && ( ★ Popular )} {s.score.toFixed(0)}
    {s.reasons.map((r, i) => ( {r.category} {r.detail} ))}
  • ))}
)} {hasMore && (
)} )}
); }