Add a "not owned" quick filter and horizontal tile view to Card Search
"Not owned" moves into the shared Quick filters icon-toggle row (hidden on the Library tab, where it'd always be a no-op since that view is owned-only already) instead of a standalone button. Tiles view gains a "Horizontal" toggle that rotates actually-horizontal cards (Battles, Planes, Phenomena) into their natural landscape orientation via a container-query-sized wrapper, spanning two grid columns so the rotated card isn't squeezed into a single portrait column's width and left too small to read. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1332,6 +1332,7 @@ export function CardTile({
|
||||
cornerBottomLeft,
|
||||
footer,
|
||||
selected,
|
||||
rotated,
|
||||
}: {
|
||||
card: CardRef;
|
||||
corner?: ReactNode;
|
||||
@@ -1342,19 +1343,34 @@ export function CardTile({
|
||||
footer?: ReactNode;
|
||||
/** Highlights the tile and keeps `cornerLeft` visible without hovering. */
|
||||
selected?: boolean;
|
||||
/** Caller wants Horizontal view — only actually rotates when `card.horizontal`
|
||||
* is also true, so mixed results (e.g. a set with both Sieges and normal
|
||||
* cards) don't spin the ones that are already right-side up. */
|
||||
rotated?: boolean;
|
||||
}) {
|
||||
const { open } = useClickPreview();
|
||||
const showRotated = !!rotated && card.horizontal;
|
||||
return (
|
||||
<div className={`tile ${selected ? "tile-selected" : ""}`}>
|
||||
<div className="tile-img-wrap">
|
||||
<div className={`tile ${selected ? "tile-selected" : ""} ${showRotated ? "tile-rotated" : ""}`}>
|
||||
<div className={showRotated ? "tile-img-wrap-rotated" : "tile-img-wrap"}>
|
||||
{card.image_uri ? (
|
||||
<img
|
||||
className="tile-img"
|
||||
src={card.image_uri}
|
||||
alt={card.name}
|
||||
loading="lazy"
|
||||
onClick={() => open(card)}
|
||||
/>
|
||||
showRotated ? (
|
||||
<img
|
||||
className="tile-img-rotated"
|
||||
src={card.image_uri}
|
||||
alt={card.name}
|
||||
loading="lazy"
|
||||
onClick={() => open(card)}
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
className="tile-img"
|
||||
src={card.image_uri}
|
||||
alt={card.name}
|
||||
loading="lazy"
|
||||
onClick={() => open(card)}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<div className="tile-noimg">{card.name}</div>
|
||||
)}
|
||||
@@ -1456,6 +1472,14 @@ const ICON_BLANK_BOX = icon(
|
||||
<line x1="3" y1="9" x2="21" y2="9" />
|
||||
</>,
|
||||
);
|
||||
/** An open package — cards you don't have a copy of yet. */
|
||||
const ICON_PACKAGE = icon(
|
||||
<>
|
||||
<path d="M21 8l-9-5-9 5 9 5 9-5z" />
|
||||
<path d="M3 8v8l9 5 9-5V8" />
|
||||
<path d="M12 13v8" />
|
||||
</>,
|
||||
);
|
||||
|
||||
/**
|
||||
* Card-origin and numeric criteria shared by the Library and Card Search
|
||||
@@ -1475,6 +1499,7 @@ export type CardCriteria = {
|
||||
legalIn: string;
|
||||
canBeCommander: boolean;
|
||||
vanillaOnly: boolean;
|
||||
notOwned: boolean;
|
||||
powerMin: string;
|
||||
powerMax: string;
|
||||
toughMin: string;
|
||||
@@ -1490,6 +1515,7 @@ export const EMPTY_CRITERIA: CardCriteria = {
|
||||
legalIn: "",
|
||||
canBeCommander: false,
|
||||
vanillaOnly: false,
|
||||
notOwned: false,
|
||||
powerMin: "",
|
||||
powerMax: "",
|
||||
toughMin: "",
|
||||
@@ -1521,6 +1547,7 @@ export function applyCardCriteria(c: CardCriteria): Partial<LibraryFilter> {
|
||||
legal_in: c.legalIn || null,
|
||||
can_be_commander: c.canBeCommander,
|
||||
vanilla_only: c.vanillaOnly,
|
||||
not_owned: c.notOwned,
|
||||
power_min: num(c.powerMin),
|
||||
power_max: num(c.powerMax),
|
||||
toughness_min: num(c.toughMin),
|
||||
@@ -1533,13 +1560,14 @@ export function applyCardCriteria(c: CardCriteria): Partial<LibraryFilter> {
|
||||
*
|
||||
* The three "exclude" toggles deliberately don't count: they refine a result
|
||||
* set rather than select one, so ticking "hide Un-sets" on its own shouldn't
|
||||
* make Card Search pull every card in the database. Choosing a format or a
|
||||
* power range is a real query, and does.
|
||||
* make Card Search pull every card in the database. Choosing a format, a
|
||||
* power range, or "not owned" is a real query, and does.
|
||||
*/
|
||||
export function criteriaActive(c: CardCriteria): boolean {
|
||||
return (
|
||||
c.canBeCommander ||
|
||||
c.vanillaOnly ||
|
||||
c.notOwned ||
|
||||
c.legalIn !== "" ||
|
||||
[c.priceMin, c.priceMax, c.powerMin, c.powerMax, c.toughMin, c.toughMax].some(
|
||||
(v) => v.trim() !== "",
|
||||
@@ -1595,9 +1623,13 @@ function RangePair({
|
||||
export function CardCriteriaFields({
|
||||
value: c,
|
||||
onChange,
|
||||
hideNotOwned,
|
||||
}: {
|
||||
value: CardCriteria;
|
||||
onChange: (patch: Partial<CardCriteria>) => void;
|
||||
/** Omit the "Not owned" toggle — for the Library tab, which already shows
|
||||
* nothing but owned cards, so the filter would just be a no-op there. */
|
||||
hideNotOwned?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
@@ -1681,6 +1713,15 @@ export function CardCriteriaFields({
|
||||
on={c.vanillaOnly}
|
||||
onToggle={() => onChange({ vanillaOnly: !c.vanillaOnly })}
|
||||
/>
|
||||
{!hideNotOwned && (
|
||||
<IconToggle
|
||||
icon={ICON_PACKAGE}
|
||||
label="Not owned"
|
||||
hint="Only cards you don't have a single copy of yet."
|
||||
on={c.notOwned}
|
||||
onToggle={() => onChange({ notOwned: !c.notOwned })}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user