Printing picker: quick-filter box for cards with many printings
Filters by set code, collector number, year, rarity, language and finish, matching each word against the start of a field. Shown when a card has 7+ printings. Closes #31 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1261,9 +1261,33 @@ function PrintingsSection({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Below this many printings the picker skips its filter box. */
|
||||||
|
const PRINTING_FILTER_MIN = 7;
|
||||||
|
|
||||||
|
/** Whether a printing matches a filter query: every word must start one of
|
||||||
|
* the things the picker shows for it — set code, collector number, year,
|
||||||
|
* rarity, language, finish. Prefixes rather than substrings, so "foil"
|
||||||
|
* doesn't also match every nonfoil printing. */
|
||||||
|
function printingMatches(p: PrintingRef, query: string): boolean {
|
||||||
|
const fields = [
|
||||||
|
p.set_code,
|
||||||
|
p.collector_number,
|
||||||
|
`#${p.collector_number}`,
|
||||||
|
p.released_at,
|
||||||
|
p.rarity,
|
||||||
|
p.lang,
|
||||||
|
...p.finishes.map((f) => FINISH_LABELS[f] ?? f),
|
||||||
|
].map(fold);
|
||||||
|
return fold(query)
|
||||||
|
.split(/\s+/)
|
||||||
|
.filter(Boolean)
|
||||||
|
.every((word) => fields.some((f) => f.startsWith(word)));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A grid of a card's printings to pick from. With `chooseFinish`, each
|
* A grid of a card's printings to pick from. With `chooseFinish`, each
|
||||||
* printing also offers its finishes (clicking the art takes the first).
|
* printing also offers its finishes (clicking the art takes the first).
|
||||||
|
* Long lists (basic lands run to hundreds) get a filter box.
|
||||||
*
|
*
|
||||||
* Opens above the click preview, which closes on any Escape it hears on
|
* Opens above the click preview, which closes on any Escape it hears on
|
||||||
* `window` — so this listens in the capture phase and stops the event there,
|
* `window` — so this listens in the capture phase and stops the event there,
|
||||||
@@ -1294,6 +1318,8 @@ function PrintingPicker({
|
|||||||
return () => window.removeEventListener("keydown", onKey, true);
|
return () => window.removeEventListener("keydown", onKey, true);
|
||||||
}, [onCancel]);
|
}, [onCancel]);
|
||||||
useBackButton(onCancel);
|
useBackButton(onCancel);
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
const shown = printings.filter((p) => printingMatches(p, query));
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<div
|
<div
|
||||||
@@ -1308,8 +1334,23 @@ function PrintingPicker({
|
|||||||
<h3 className="dialog-title">{chooseFinish ? `Add a printing of ${name}` : `Show ${name} as`}</h3>
|
<h3 className="dialog-title">{chooseFinish ? `Add a printing of ${name}` : `Show ${name} as`}</h3>
|
||||||
<IconButton icon={Icons.close} label="Close" onClick={onCancel} />
|
<IconButton icon={Icons.close} label="Close" onClick={onCancel} />
|
||||||
</div>
|
</div>
|
||||||
|
{printings.length >= PRINTING_FILTER_MIN && (
|
||||||
|
<div className="printing-filter">
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
value={query}
|
||||||
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
|
placeholder="Filter by set, number, year, rarity, finish…"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<span className="muted">
|
||||||
|
{shown.length === printings.length ? printings.length : `${shown.length} of ${printings.length}`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{shown.length === 0 && <p className="hint">No printings match.</p>}
|
||||||
<div className="printing-grid">
|
<div className="printing-grid">
|
||||||
{printings.map((p) => {
|
{shown.map((p) => {
|
||||||
const finishes = p.finishes.length > 0 ? p.finishes : ["nonfoil"];
|
const finishes = p.finishes.length > 0 ? p.finishes : ["nonfoil"];
|
||||||
const meta = [
|
const meta = [
|
||||||
p.released_at.slice(0, 4),
|
p.released_at.slice(0, 4),
|
||||||
|
|||||||
@@ -3949,6 +3949,16 @@ button.pull-action {
|
|||||||
.printing-picker-backdrop {
|
.printing-picker-backdrop {
|
||||||
z-index: 500;
|
z-index: 500;
|
||||||
}
|
}
|
||||||
|
.printing-filter {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
margin-bottom: var(--space-3);
|
||||||
|
}
|
||||||
|
.printing-filter input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
.printing-grid {
|
.printing-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(min(130px, 40vw), 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(min(130px, 40vw), 1fr));
|
||||||
|
|||||||
Reference in New Issue
Block a user