Shared card filter panel: News set browser gets every Library filter
Extract the filter fields and LibraryFilter-building state into src/cardFilter.tsx (CardFilterPanel + useCardFilter) and use it in the Library, Card Search and News set browser, replacing three diverging copies. search_reference now honors in_deck/location too, so every filter works in every tab. Closes #13
This commit is contained in:
@@ -6,7 +6,6 @@ import {
|
||||
Deck,
|
||||
LibraryCard,
|
||||
LibraryFacets,
|
||||
LibraryFilter,
|
||||
LibraryImportResult,
|
||||
PreviewContext,
|
||||
} from "../api";
|
||||
@@ -15,7 +14,6 @@ import {
|
||||
CardTile,
|
||||
DialogFrame,
|
||||
ColorPips,
|
||||
ColorToggles,
|
||||
GroupBySelect,
|
||||
GroupMode,
|
||||
InfoDialog,
|
||||
@@ -26,26 +24,21 @@ import {
|
||||
SortBySelect,
|
||||
SortDir,
|
||||
SortMode,
|
||||
TagInput,
|
||||
TileColumnsControl,
|
||||
ViewToggle,
|
||||
CardCriteriaFields,
|
||||
applyCardCriteria,
|
||||
groupCards,
|
||||
sortCards,
|
||||
useCardCriteria,
|
||||
usePersistedState,
|
||||
useTileColumns,
|
||||
useViewMode,
|
||||
} from "../components";
|
||||
import { CardFilterPanel, NO_FACETS, useCardFilter } from "../cardFilter";
|
||||
|
||||
// How long a card that's been decremented to 0 lingers (dimmed, still
|
||||
// adjustable) before actually dropping out of the library view — gives the
|
||||
// user a window to undo an accidental removal by clicking + again.
|
||||
const REMOVAL_GRACE_MS = 5000;
|
||||
|
||||
const NO_FACETS: LibraryFacets = { types: [], subtypes: [], keywords: [] };
|
||||
|
||||
/**
|
||||
* Browse & filter the owned library by name and card criteria.
|
||||
*
|
||||
@@ -59,25 +52,8 @@ export function LibraryBrowser({
|
||||
}: {
|
||||
onOfferUndo: (message: string, run: () => void | Promise<void>) => void;
|
||||
}) {
|
||||
const [text, setText] = useState("");
|
||||
const [commander, setCommander] = useState(0);
|
||||
const [colorsInclude, setColorsInclude] = useState(0);
|
||||
// Colourlessness can't ride in the bitmask — 0 there means "no filter".
|
||||
const [colorlessIdentity, setColorlessIdentity] = useState(false);
|
||||
const [colorlessColors, setColorlessColors] = useState(false);
|
||||
const [cmcMin, setCmcMin] = useState("");
|
||||
const [cmcMax, setCmcMax] = useState("");
|
||||
const [types, setTypes] = useState<string[]>([]);
|
||||
const [subtypes, setSubtypes] = useState<string[]>([]);
|
||||
const [keywords, setKeywords] = useState<string[]>([]);
|
||||
const [inDeck, setInDeck] = useState<number | "">("");
|
||||
const [location, setLocation] = useState("");
|
||||
const [oracleText, setOracleText] = useState("");
|
||||
const [criteria, patchCriteria] = useCardCriteria("library-criteria");
|
||||
// Collapsed by default (relevant on narrow/mobile screens only — see
|
||||
// .filters-more's CSS, which is always expanded above that breakpoint
|
||||
// regardless of this).
|
||||
const [filtersExpanded, setFiltersExpanded] = useState(false);
|
||||
const filterState = useCardFilter("library-criteria");
|
||||
const { filter, criteria } = filterState;
|
||||
const [facets, setFacets] = useState<LibraryFacets>(NO_FACETS);
|
||||
|
||||
const [decks, setDecks] = useState<Deck[]>([]);
|
||||
@@ -142,43 +118,11 @@ export function LibraryBrowser({
|
||||
}, [selected, rows]);
|
||||
|
||||
useEffect(() => {
|
||||
const filter: LibraryFilter = {
|
||||
text: text.trim() || null,
|
||||
commander_identity: commander || null,
|
||||
colors_include: colorsInclude || null,
|
||||
colorless_identity: colorlessIdentity,
|
||||
colorless_colors: colorlessColors,
|
||||
cmc_min: cmcMin === "" ? null : Number(cmcMin),
|
||||
cmc_max: cmcMax === "" ? null : Number(cmcMax),
|
||||
types,
|
||||
subtypes,
|
||||
keywords,
|
||||
in_deck: inDeck === "" ? null : Number(inDeck),
|
||||
location: location.trim() || null,
|
||||
oracle_text: oracleText.trim() || null,
|
||||
...applyCardCriteria(criteria),
|
||||
};
|
||||
const t = setTimeout(() => {
|
||||
api.searchLibrary(filter).then(setRows).catch(() => setRows([]));
|
||||
}, 120);
|
||||
return () => clearTimeout(t);
|
||||
}, [
|
||||
text,
|
||||
commander,
|
||||
colorsInclude,
|
||||
colorlessIdentity,
|
||||
colorlessColors,
|
||||
cmcMin,
|
||||
cmcMax,
|
||||
types,
|
||||
subtypes,
|
||||
keywords,
|
||||
inDeck,
|
||||
location,
|
||||
oracleText,
|
||||
criteria,
|
||||
refreshKey,
|
||||
]);
|
||||
}, [filter, refreshKey]);
|
||||
|
||||
async function adjust(oracleId: string, delta: number) {
|
||||
const qty = await api.addOwned(oracleId, delta);
|
||||
@@ -661,110 +605,7 @@ export function LibraryBrowser({
|
||||
)}
|
||||
|
||||
<div className="panel workspace">
|
||||
<div className="filters">
|
||||
<div className="field">
|
||||
<label>Name contains</label>
|
||||
<input value={text} onChange={(e) => setText(e.target.value)} placeholder="e.g. bolt" />
|
||||
</div>
|
||||
<div className={`filters-more ${filtersExpanded ? "expanded" : ""}`}>
|
||||
<div className="field">
|
||||
<label>Playable in commander (color identity ⊆)</label>
|
||||
<ColorToggles
|
||||
value={commander}
|
||||
onChange={setCommander}
|
||||
colorless={colorlessIdentity}
|
||||
onColorlessChange={setColorlessIdentity}
|
||||
colorlessTitle="Colorless commander — only cards with no color identity"
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label>Includes colors</label>
|
||||
<ColorToggles
|
||||
value={colorsInclude}
|
||||
onChange={setColorsInclude}
|
||||
colorless={colorlessColors}
|
||||
onColorlessChange={setColorlessColors}
|
||||
colorlessTitle="Colorless cards only"
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label>Mana value</label>
|
||||
<div className="row">
|
||||
<input
|
||||
type="number"
|
||||
style={{ width: 70 }}
|
||||
placeholder="min"
|
||||
value={cmcMin}
|
||||
onChange={(e) => setCmcMin(e.target.value)}
|
||||
/>
|
||||
<span className="muted">–</span>
|
||||
<input
|
||||
type="number"
|
||||
style={{ width: 70 }}
|
||||
placeholder="max"
|
||||
value={cmcMax}
|
||||
onChange={(e) => setCmcMax(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label>Type(s)</label>
|
||||
<TagInput values={types} onChange={setTypes} suggestions={facets.types} placeholder="Creature…" />
|
||||
</div>
|
||||
<div className="field">
|
||||
<label>Subtype(s)</label>
|
||||
<TagInput
|
||||
values={subtypes}
|
||||
onChange={setSubtypes}
|
||||
suggestions={facets.subtypes}
|
||||
placeholder="Elf…"
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label>Keyword(s)</label>
|
||||
<TagInput
|
||||
values={keywords}
|
||||
onChange={setKeywords}
|
||||
suggestions={facets.keywords}
|
||||
placeholder="Flying…"
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label>In deck</label>
|
||||
<select value={inDeck} onChange={(e) => setInDeck(e.target.value === "" ? "" : Number(e.target.value))}>
|
||||
<option value="">Any</option>
|
||||
{decks.map((d) => (
|
||||
<option key={d.id} value={d.id}>
|
||||
{d.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label>Location</label>
|
||||
<input
|
||||
value={location}
|
||||
onChange={(e) => setLocation(e.target.value)}
|
||||
placeholder="e.g. Box 1"
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label>Oracle text</label>
|
||||
<input
|
||||
value={oracleText}
|
||||
onChange={(e) => setOracleText(e.target.value)}
|
||||
placeholder="e.g. landfall"
|
||||
/>
|
||||
</div>
|
||||
<CardCriteriaFields value={criteria} onChange={patchCriteria} hideNotOwned showSpares />
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="filters-toggle"
|
||||
onClick={() => setFiltersExpanded((v) => !v)}
|
||||
>
|
||||
{filtersExpanded ? "▴ Fewer filters" : "▾ More filters"}
|
||||
</button>
|
||||
<CardFilterPanel state={filterState} facets={facets} decks={decks} hideNotOwned />
|
||||
|
||||
<div className="workspace-bar">
|
||||
<label className="workspace-bar-check" title="Select all filtered cards">
|
||||
|
||||
Reference in New Issue
Block a user