Add card USD pricing and a deck maybeboard
Prices come from Scryfall's prices.usd on printings, shown as a hover badge on card tiles. The price is picked independently of whichever printing supplies the display image (the cheapest known price across all printings), since the display printing is often a promo/variant with no listed price. The maybeboard lets a deck track cards under consideration without counting them in deck analysis stats or library assignment totals. Cards move between mainboard and maybeboard via a per-card toggle; maybeboarding the commander clears commander status. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,13 @@ import {
|
||||
useViewMode,
|
||||
} from "../components";
|
||||
|
||||
/** Tooltip/label + icon for the per-card mainboard/maybeboard toggle. */
|
||||
function boardToggleProps(e: DeckEntry) {
|
||||
return e.is_maybeboard
|
||||
? { icon: Icons.arrowLeftCircle, label: "Move to deck" }
|
||||
: { icon: Icons.helpCircle, label: "Move to maybeboard" };
|
||||
}
|
||||
|
||||
type DeckDialog =
|
||||
| { kind: "delete"; deck: Deck }
|
||||
| { kind: "rename"; deck: Deck }
|
||||
@@ -55,11 +62,12 @@ interface DeckStats {
|
||||
|
||||
/**
|
||||
* Deck composition stats, excluding the commander (shown separately in its
|
||||
* own hero display) — matches the usual "99 + commander" framing even for
|
||||
* non-Commander decks, where it's just "everything but the flagged card".
|
||||
* own hero display) and the maybeboard (cards under consideration, not
|
||||
* actually part of the deck) — matches the usual "99 + commander" framing
|
||||
* even for non-Commander decks, where it's just "everything but those".
|
||||
*/
|
||||
function computeDeckStats(contents: DeckEntry[]): DeckStats {
|
||||
const cards = contents.filter((e) => !e.is_commander);
|
||||
const cards = contents.filter((e) => !e.is_commander && !e.is_maybeboard);
|
||||
const totalCards = cards.reduce((sum, e) => sum + e.quantity, 0);
|
||||
const lands = cards.filter((e) => e.type_line.includes("Land"));
|
||||
const nonlands = cards.filter((e) => !e.type_line.includes("Land"));
|
||||
@@ -208,6 +216,8 @@ export function DeckView() {
|
||||
const [anyResults, setAnyResults] = useState<CardRef[]>([]);
|
||||
const [cmdQuery, setCmdQuery] = useState("");
|
||||
const [cmdResults, setCmdResults] = useState<CardRef[]>([]);
|
||||
const [maybeQuery, setMaybeQuery] = useState("");
|
||||
const [maybeResults, setMaybeResults] = useState<CardRef[]>([]);
|
||||
|
||||
const [pendingRemoval, setPendingRemoval] = useState<Set<string>>(new Set());
|
||||
const removalTimers = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map());
|
||||
@@ -278,6 +288,13 @@ export function DeckView() {
|
||||
return () => clearTimeout(t);
|
||||
}, [anyQuery]);
|
||||
|
||||
useEffect(() => {
|
||||
const q = maybeQuery.trim();
|
||||
if (!q) return setMaybeResults([]);
|
||||
const t = setTimeout(() => api.searchCards(q, 12).then(setMaybeResults), 80);
|
||||
return () => clearTimeout(t);
|
||||
}, [maybeQuery]);
|
||||
|
||||
useEffect(() => {
|
||||
const q = cmdQuery.trim();
|
||||
if (!q) return setCmdResults([]);
|
||||
@@ -297,7 +314,7 @@ export function DeckView() {
|
||||
if (selected === null) return;
|
||||
const existing = contents.find((e) => e.oracle_id === card.oracle_id);
|
||||
const qty = existing ? existing.quantity + 1 : 1;
|
||||
await api.setDeckCard(selected, card.oracle_id, qty, false);
|
||||
await api.setDeckCard(selected, card.oracle_id, qty, false, false);
|
||||
setLibQuery("");
|
||||
setLibResults([]);
|
||||
setAnyQuery("");
|
||||
@@ -305,10 +322,40 @@ export function DeckView() {
|
||||
await loadContents();
|
||||
}
|
||||
|
||||
async function addToMaybeboard(card: CardRef) {
|
||||
if (selected === null) return;
|
||||
const existing = contents.find((e) => e.oracle_id === card.oracle_id);
|
||||
const qty = existing ? existing.quantity + 1 : 1;
|
||||
await api.setDeckCard(selected, card.oracle_id, qty, false, true);
|
||||
setMaybeQuery("");
|
||||
setMaybeResults([]);
|
||||
await loadContents();
|
||||
}
|
||||
|
||||
/** Move a card between the mainboard and the maybeboard. Clears commander
|
||||
* status (and the deck's commander, if this was it) when maybeboarding. */
|
||||
async function setMaybeboard(entry: DeckEntry, isMaybeboard: boolean) {
|
||||
if (selected === null) return;
|
||||
const isCommander = isMaybeboard ? false : entry.is_commander;
|
||||
await api.setDeckCard(selected, entry.oracle_id, entry.quantity, isCommander, isMaybeboard);
|
||||
if (isMaybeboard && deck?.commander_oracle_id === entry.oracle_id) {
|
||||
await api.setDeckCommander(selected, null);
|
||||
await loadDecks();
|
||||
}
|
||||
await loadContents();
|
||||
}
|
||||
|
||||
async function setQty(oracleId: string, qty: number) {
|
||||
if (selected === null) return;
|
||||
const clamped = Math.max(0, qty);
|
||||
await api.setDeckCard(selected, oracleId, clamped, false);
|
||||
const existing = contents.find((e) => e.oracle_id === oracleId);
|
||||
await api.setDeckCard(
|
||||
selected,
|
||||
oracleId,
|
||||
clamped,
|
||||
existing?.is_commander ?? false,
|
||||
existing?.is_maybeboard ?? false,
|
||||
);
|
||||
setContents((cs) => cs.map((e) => (e.oracle_id === oracleId ? { ...e, quantity: clamped } : e)));
|
||||
|
||||
// Re-adding during the grace period below cancels the pending removal.
|
||||
@@ -408,10 +455,10 @@ export function DeckView() {
|
||||
|
||||
async function setCommander(card: CardRef) {
|
||||
if (selected === null) return;
|
||||
// Ensure the commander is in the deck, then flag it.
|
||||
if (!contents.find((e) => e.oracle_id === card.oracle_id)) {
|
||||
await api.setDeckCard(selected, card.oracle_id, 1, true);
|
||||
}
|
||||
// Ensure the card is in the deck (mainboard, not maybeboard) and flagged
|
||||
// commander — covers both "not in the deck yet" and "was maybeboarded".
|
||||
const existing = contents.find((e) => e.oracle_id === card.oracle_id);
|
||||
await api.setDeckCard(selected, card.oracle_id, existing?.quantity ?? 1, true, false);
|
||||
await api.setDeckCommander(selected, card.oracle_id);
|
||||
setCmdQuery("");
|
||||
setCmdResults([]);
|
||||
@@ -426,8 +473,9 @@ export function DeckView() {
|
||||
await loadDecks();
|
||||
}
|
||||
|
||||
const owned = contents.filter((e) => e.quantity_owned > 0);
|
||||
const unowned = contents.filter((e) => e.quantity_owned === 0);
|
||||
const owned = contents.filter((e) => !e.is_maybeboard && e.quantity_owned > 0);
|
||||
const unowned = contents.filter((e) => !e.is_maybeboard && e.quantity_owned === 0);
|
||||
const maybeboard = contents.filter((e) => e.is_maybeboard);
|
||||
const commanderEntry = contents.find((e) => e.oracle_id === deck?.commander_oracle_id);
|
||||
const deckStats = useMemo(() => computeDeckStats(contents), [contents]);
|
||||
|
||||
@@ -458,7 +506,13 @@ export function DeckView() {
|
||||
pending={pendingRemoval.has(e.oracle_id)}
|
||||
/>
|
||||
}
|
||||
footer={e.is_commander ? <span className="tile-badge">★ commander</span> : undefined}
|
||||
footer={
|
||||
<>
|
||||
{e.is_commander && <span className="tile-badge">★ commander</span>}
|
||||
<span className="spacer" />
|
||||
<IconButton {...boardToggleProps(e)} onClick={() => setMaybeboard(e, !e.is_maybeboard)} />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -477,6 +531,7 @@ export function DeckView() {
|
||||
<th>Type</th>
|
||||
<th>In deck</th>
|
||||
<th>Owned</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -510,6 +565,9 @@ export function DeckView() {
|
||||
/>
|
||||
</td>
|
||||
<td>{e.quantity_owned}</td>
|
||||
<td>
|
||||
<IconButton {...boardToggleProps(e)} onClick={() => setMaybeboard(e, !e.is_maybeboard)} />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@@ -737,6 +795,23 @@ export function DeckView() {
|
||||
/>
|
||||
{cardsGroup(unowned)}
|
||||
</section>
|
||||
|
||||
<section style={{ marginTop: 26 }}>
|
||||
<h3>
|
||||
Maybeboard <span className="badge">{maybeboard.length}</span>
|
||||
</h3>
|
||||
<p className="hint" style={{ marginTop: -4 }}>
|
||||
Cards under consideration — not counted in deck analysis.
|
||||
</p>
|
||||
<CardSearchBox
|
||||
placeholder="Search all cards to add one to the maybeboard…"
|
||||
value={maybeQuery}
|
||||
onChange={setMaybeQuery}
|
||||
results={maybeResults}
|
||||
onPick={addToMaybeboard}
|
||||
/>
|
||||
{cardsGroup(maybeboard)}
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user