Moxfield import

This commit is contained in:
2026-07-10 20:54:39 -05:00
parent f22df0e1d4
commit f18c0c6564
7 changed files with 416 additions and 61 deletions
+72 -15
View File
@@ -44,6 +44,10 @@ textarea { resize: vertical; }
.badge-running { color: var(--yellow); background: #1e1a0e; }
.badge-queued { color: var(--blue); background: #101622; }
.badge-failed { color: var(--red); background: #1e1010; }
.badge-source { color: var(--blue); background: #101622; border: 1px solid #1e2e3a; }
.mode-toggle .btn.active { color: var(--green); border-color: #1e3a1e; background: #141e14; }
.token-badge { position: absolute; top: 3px; right: 3px; font-size: 9px; letter-spacing: 0.5px; color: var(--blue); background: rgba(0,0,0,0.78); padding: 2px 5px; border-radius: 2px; pointer-events: none; }
.edit-mode .token-badge { display: none; }
/* Main panel */
.panel { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 0; }
@@ -131,10 +135,20 @@ textarea { resize: vertical; }
<div class="sidebar">
<div class="section">
<div class="section-label">New Deck</div>
<input id="deckName" placeholder="Deck name" />
<label class="token-toggle"><input type="checkbox" id="isTokens" onchange="onTokenToggle()" /> Token bundle (no commander)</label>
<div id="commanderWrap"><input id="deckCommander" placeholder="Commander (defaults to first card)" /></div>
<textarea id="cardList" placeholder="One per line — card name or Scryfall card URL"></textarea>
<div class="row mode-toggle" style="margin-top:0">
<button class="btn active" id="modeManualBtn" onclick="setCreateMode('manual')" style="flex:1">Manual</button>
<button class="btn" id="modeMoxBtn" onclick="setCreateMode('moxfield')" style="flex:1">Moxfield</button>
</div>
<div id="manualCreateFields" style="margin-top:7px">
<input id="deckName" placeholder="Deck name" />
<label class="token-toggle"><input type="checkbox" id="isTokens" onchange="onTokenToggle()" /> Token bundle (no commander)</label>
<div id="commanderWrap"><input id="deckCommander" placeholder="Commander (defaults to first card)" /></div>
<textarea id="cardList" placeholder="One per line — card name or Scryfall card URL"></textarea>
</div>
<div id="moxfieldCreateFields" style="display:none; margin-top:7px">
<input id="moxUrl" placeholder="Moxfield deck URL or ID" />
<label class="token-toggle"><input type="checkbox" id="moxIncludeTokens" /> Pull tokens too (for TTS)</label>
</div>
<div class="row">
<button class="btn btn-green" id="submitBtn" onclick="submitDeck()" style="flex:1">Submit</button>
<button class="btn" onclick="clearForm()">Clear</button>
@@ -151,9 +165,11 @@ textarea { resize: vertical; }
<div class="progress-bar"><div class="progress-fill" id="progressFill" style="width:0"></div></div>
<div class="deck-header" id="deckHeader" style="display:none">
<span class="deck-header-title" id="headerTitle"></span>
<span class="badge badge-source" id="sourceBadge" style="display:none">MOX</span>
<span class="deck-header-meta" id="headerMeta"></span>
<span class="price-tag" id="priceTag"></span>
<button class="btn btn-gold" id="editBtn" onclick="enterEditMode()" style="display:none">Edit</button>
<button class="btn btn-gold" id="syncBtn" onclick="syncMoxfieldDeck()" style="display:none">Sync</button>
<button class="btn btn-green" id="saveBtn" onclick="saveEdit()" style="display:none">Save</button>
<button class="btn" id="cancelBtn" onclick="exitEditMode(true)" style="display:none">Cancel</button>
<button class="btn" id="sortBtn" onclick="toggleSort()" style="display:none">Sort: Default</button>
@@ -261,20 +277,38 @@ async function selectDeck(slug) {
function updateHeader(d) {
document.getElementById("headerTitle").textContent = d.deck_name;
document.getElementById("sourceBadge").style.display = d.source === "moxfield" ? "" : "none";
const pct = d.total > 0 ? d.done / d.total * 100 : (d.status === "complete" ? 100 : 0);
document.getElementById("progressFill").style.width = pct + "%";
document.getElementById("headerMeta").textContent = d.total > 0 ? `${d.done}/${d.total}` : "";
document.getElementById("priceTag").textContent = d.price_usd > 0 ? `$${d.price_usd.toFixed(2)}` : "";
const complete = d.status === "complete";
const terminal = complete || d.status === "failed";
const isMox = d.source === "moxfield";
document.getElementById("dlBtn").style.display = complete && !editMode ? "" : "none";
document.getElementById("delBtn").style.display = terminal && !editMode ? "" : "none";
document.getElementById("editBtn").style.display = terminal && !editMode ? "" : "none";
document.getElementById("editBtn").style.display = terminal && !editMode && !isMox ? "" : "none";
document.getElementById("syncBtn").style.display = terminal && !editMode && isMox ? "" : "none";
document.getElementById("saveBtn").style.display = editMode ? "" : "none";
document.getElementById("cancelBtn").style.display = editMode ? "" : "none";
document.getElementById("sortBtn").style.display = !editMode ? "" : "none";
}
async function syncMoxfieldDeck() {
if (!activeDeck) return;
const btn = document.getElementById("syncBtn");
btn.disabled = true;
try {
await api("POST", `/decks/${activeDeck}/moxfield-sync`);
await loadDecks();
await selectDeck(activeDeck);
} catch (e) {
alert(e.message);
} finally {
btn.disabled = false;
}
}
async function refreshCards(slug) {
if (activeDeck !== slug) return;
try {
@@ -310,7 +344,7 @@ function renderCards(slug, cards) {
const isRemoved = editRemovals.has(card.id);
const label = cardLabel(card);
const priceStr = card.price_usd > 0 ? `$${card.price_usd.toFixed(2)}` : null;
const tileKey = [card.filename, card.exists, card.back_exists, isCommander, isRemoved, editMode, card.fetch_status, label, (card.price_usd || 0)].join("|");
const tileKey = [card.filename, card.exists, card.back_exists, isCommander, isRemoved, editMode, card.fetch_status, label, (card.price_usd || 0), card.is_token].join("|");
if (!tile) { tile = document.createElement("div"); grid.appendChild(tile); tile.dataset.key = ""; }
@@ -324,6 +358,7 @@ function renderCards(slug, cards) {
<img src="${imgUrl}" loading="lazy" alt="${esc(label)}" onclick="showLightbox('${imgUrl}')">
<span class="card-label">${isCommander ? "♛ " : ""}${esc(label)}</span>
${isCommander ? `<span class="commander-crown">♛</span>` : ""}
${card.is_token ? `<span class="token-badge">TOKEN</span>` : ""}
${card.back_exists ? `<div class="dfc-badge" title="flip side" onclick="event.stopPropagation();showLightbox('/cards/${slug}/${esc(card.back_filename)}')">⇄</div>` : ""}
${priceStr ? `<span class="card-price">${priceStr}</span>` : `<span class="card-price free">$0</span>`}
<button class="card-overlay-btn btn-remove-card" onclick="toggleRemove(${card.id})">✕</button>
@@ -561,19 +596,40 @@ function showLightbox(url) {
function closeLightbox() { document.getElementById("lightbox").classList.remove("open"); }
document.addEventListener("keydown", e => { if (e.key === "Escape") closeLightbox(); });
let createMode = "manual";
function setCreateMode(mode) {
createMode = mode;
document.getElementById("manualCreateFields").style.display = mode === "manual" ? "" : "none";
document.getElementById("moxfieldCreateFields").style.display = mode === "moxfield" ? "" : "none";
document.getElementById("modeManualBtn").classList.toggle("active", mode === "manual");
document.getElementById("modeMoxBtn").classList.toggle("active", mode === "moxfield");
document.getElementById("submitError").style.display = "none";
}
async function submitDeck() {
const name = document.getElementById("deckName").value.trim();
const cards = document.getElementById("cardList").value.trim();
const isTokens = document.getElementById("isTokens").checked;
const commander = document.getElementById("deckCommander").value.trim();
const err = document.getElementById("submitError");
err.style.display = "none";
if (!name) { err.textContent = "Need a name."; err.style.display = ""; return; }
if (!isTokens && !cards) { err.textContent = "Need a card list (or check Token bundle)."; err.style.display = ""; return; }
const btn = document.getElementById("submitBtn");
btn.disabled = true;
let r;
try {
const r = await api("POST", "/decks", { deck_name: name, cards, commander, is_tokens: isTokens });
if (createMode === "moxfield") {
const url = document.getElementById("moxUrl").value.trim();
const includeTokens = document.getElementById("moxIncludeTokens").checked;
if (!url) { err.textContent = "Need a Moxfield deck URL."; err.style.display = ""; return; }
btn.disabled = true;
r = await api("POST", "/decks/moxfield", { url, include_tokens: includeTokens });
} else {
const name = document.getElementById("deckName").value.trim();
const cards = document.getElementById("cardList").value.trim();
const isTokens = document.getElementById("isTokens").checked;
const commander = document.getElementById("deckCommander").value.trim();
if (!name) { err.textContent = "Need a name."; err.style.display = ""; return; }
if (!isTokens && !cards) { err.textContent = "Need a card list (or check Token bundle)."; err.style.display = ""; return; }
btn.disabled = true;
r = await api("POST", "/decks", { deck_name: name, cards, commander, is_tokens: isTokens });
}
clearForm();
await loadDecks();
selectDeck(r.slug);
@@ -589,8 +645,9 @@ function onTokenToggle() {
}
function clearForm() {
["deckName","deckCommander","cardList"].forEach(id => document.getElementById(id).value = "");
["deckName","deckCommander","cardList","moxUrl"].forEach(id => document.getElementById(id).value = "");
document.getElementById("isTokens").checked = false;
document.getElementById("moxIncludeTokens").checked = false;
onTokenToggle();
document.getElementById("submitError").style.display = "none";
}