Filter and tokens update
This commit is contained in:
+84
-19
@@ -65,6 +65,12 @@ textarea { resize: vertical; }
|
||||
.card-label { position: absolute; bottom: 0; left: 0; right: 0; background: rgba(0,0,0,0.82); color: #fff; font-size: 9px; padding: 3px 5px; opacity: 0; transition: opacity 0.15s; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.card-tile:hover .card-label { opacity: 1; }
|
||||
.commander-crown { position: absolute; top: 3px; left: 3px; font-size: 13px; line-height: 1; pointer-events: none; text-shadow: 0 1px 3px rgba(0,0,0,0.9); }
|
||||
.token-toggle { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--muted); margin-bottom: 6px; cursor: pointer; user-select: none; }
|
||||
.token-toggle input { width: auto; margin: 0; }
|
||||
.dfc-badge { position: absolute; bottom: 3px; left: 3px; background: rgba(0,0,0,0.78); color: var(--blue); font-size: 10px; line-height: 1; padding: 3px 5px; border-radius: 2px; cursor: pointer; z-index: 2; }
|
||||
.dfc-badge:hover { background: rgba(20,28,40,0.95); }
|
||||
.card-price { position: absolute; bottom: 0; right: 0; background: rgba(0,0,0,0.75); color: var(--green); font-size: 9px; padding: 2px 4px; line-height: 1.4; pointer-events: none; }
|
||||
.card-price.free { color: var(--muted); }
|
||||
.card-overlay-btn { position: absolute; display: none; border: none; cursor: pointer; font-size: 13px; line-height: 1; padding: 3px 5px; border-radius: 2px; }
|
||||
.btn-remove-card { top: 3px; right: 3px; background: rgba(180,0,0,0.8); color: #fff; }
|
||||
.btn-remove-card:hover { background: rgba(220,0,0,0.95); }
|
||||
@@ -126,8 +132,9 @@ textarea { resize: vertical; }
|
||||
<div class="section">
|
||||
<div class="section-label">New Deck</div>
|
||||
<input id="deckName" placeholder="Deck name" />
|
||||
<input id="deckCommander" placeholder="Commander (defaults to first card)" />
|
||||
<textarea id="cardList" placeholder="Paste card list — one per line"></textarea>
|
||||
<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">
|
||||
<button class="btn btn-green" id="submitBtn" onclick="submitDeck()" style="flex:1">Submit</button>
|
||||
<button class="btn" onclick="clearForm()">Clear</button>
|
||||
@@ -149,7 +156,8 @@ textarea { resize: vertical; }
|
||||
<button class="btn btn-gold" id="editBtn" onclick="enterEditMode()" style="display:none">Edit</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 btn-blue" id="dlBtn" onclick="downloadTTS()" style="display:none">↓ TTS</button>
|
||||
<button class="btn" id="sortBtn" onclick="toggleSort()" style="display:none">Sort: Default</button>
|
||||
<button class="btn btn-blue" id="dlBtn" onclick="copyTTSUrl()" style="display:none">Copy URL</button>
|
||||
<button class="btn btn-red" id="delBtn" onclick="deleteDeck()" style="display:none">Delete</button>
|
||||
</div>
|
||||
<div class="card-area" id="cardArea"><div class="placeholder">Select a deck or submit a new one.</div></div>
|
||||
@@ -171,6 +179,23 @@ let activeDeck = null, sse = null, cardPollTimer = null;
|
||||
let deckData = {}, currentCards = [];
|
||||
let editMode = false, editRemovals = new Set(), editCommander = null;
|
||||
let searchTimer = null;
|
||||
let sortOrder = "default"; // "default" | "cost-desc" | "cost-asc"
|
||||
|
||||
function toggleSort() {
|
||||
sortOrder = sortOrder === "default" ? "cost-desc" : sortOrder === "cost-desc" ? "cost-asc" : "default";
|
||||
const labels = { "default": "Sort: Default", "cost-desc": "Sort: $ High", "cost-asc": "Sort: $ Low" };
|
||||
document.getElementById("sortBtn").textContent = labels[sortOrder];
|
||||
if (activeDeck) renderCards(activeDeck, currentCards);
|
||||
}
|
||||
|
||||
function sortedCards(cards) {
|
||||
if (sortOrder === "default") return cards;
|
||||
const sorted = [...cards];
|
||||
sorted.sort((a, b) => sortOrder === "cost-desc"
|
||||
? (b.price_usd || 0) - (a.price_usd || 0)
|
||||
: (a.price_usd || 0) - (b.price_usd || 0));
|
||||
return sorted;
|
||||
}
|
||||
|
||||
async function api(method, path, body) {
|
||||
const r = await fetch(path, {
|
||||
@@ -213,6 +238,8 @@ async function selectDeck(slug) {
|
||||
if (sse) { sse.close(); sse = null; }
|
||||
if (cardPollTimer) { clearInterval(cardPollTimer); cardPollTimer = null; }
|
||||
activeDeck = slug;
|
||||
sortOrder = "default";
|
||||
document.getElementById("sortBtn").textContent = "Sort: Default";
|
||||
renderList();
|
||||
const d = deckData[slug];
|
||||
if (!d) return;
|
||||
@@ -245,6 +272,7 @@ function updateHeader(d) {
|
||||
document.getElementById("editBtn").style.display = terminal && !editMode ? "" : "none";
|
||||
document.getElementById("saveBtn").style.display = editMode ? "" : "none";
|
||||
document.getElementById("cancelBtn").style.display = editMode ? "" : "none";
|
||||
document.getElementById("sortBtn").style.display = !editMode ? "" : "none";
|
||||
}
|
||||
|
||||
async function refreshCards(slug) {
|
||||
@@ -258,7 +286,8 @@ async function refreshCards(slug) {
|
||||
|
||||
function renderCards(slug, cards) {
|
||||
const area = document.getElementById("cardArea");
|
||||
if (!cards.length) { area.innerHTML = '<div class="placeholder">No cards yet.</div>'; return; }
|
||||
const displayCards = sortedCards(cards);
|
||||
if (!displayCards.length) { area.innerHTML = '<div class="placeholder">No cards yet.</div>'; return; }
|
||||
|
||||
let grid = area.querySelector(".card-grid");
|
||||
if (!grid) {
|
||||
@@ -270,15 +299,18 @@ function renderCards(slug, cards) {
|
||||
grid.className = "card-grid" + (editMode ? " edit-mode" : "");
|
||||
}
|
||||
|
||||
const commander = editCommander || (deckData[slug] && deckData[slug].commander) || "";
|
||||
const isBundle = !(deckData[slug] && deckData[slug].commander);
|
||||
const commander = isBundle ? "" : (editCommander || deckData[slug].commander || "");
|
||||
|
||||
// Sync tiles
|
||||
const existing = Array.from(grid.querySelectorAll(".card-tile, .card-tile.missing"));
|
||||
cards.forEach((card, i) => {
|
||||
displayCards.forEach((card, i) => {
|
||||
let tile = existing[i];
|
||||
const isCommander = card.name === commander;
|
||||
const isCommander = !isBundle && card.name === commander;
|
||||
const isRemoved = editRemovals.has(card.id);
|
||||
const tileKey = card.filename + "|" + card.exists + "|" + isCommander + "|" + isRemoved + "|" + editMode;
|
||||
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("|");
|
||||
|
||||
if (!tile) { tile = document.createElement("div"); grid.appendChild(tile); tile.dataset.key = ""; }
|
||||
|
||||
@@ -289,22 +321,25 @@ function renderCards(slug, cards) {
|
||||
const imgUrl = `/cards/${slug}/${card.filename}`;
|
||||
tile.className = "card-tile" + (isCommander ? " is-commander" : "") + (isRemoved ? " marked-remove" : "");
|
||||
tile.innerHTML = `
|
||||
<img src="${imgUrl}" loading="lazy" alt="${esc(card.name)}" onclick="showLightbox('${imgUrl}')">
|
||||
<span class="card-label">${isCommander ? "♛ " : ""}${esc(card.name)}</span>
|
||||
<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.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>
|
||||
${!isCommander ? `<button class="card-overlay-btn btn-set-commander" onclick="setCommander('${esc(card.name)}')">♛</button>` : ""}
|
||||
${!isCommander && !isBundle ? `<button class="card-overlay-btn btn-set-commander" onclick="setCommander('${esc(card.name).replace(/'/g,"\\'")}')">♛</button>` : ""}
|
||||
`;
|
||||
} else {
|
||||
const hint = card.fetch_status === "failed" ? " — failed" : " …";
|
||||
tile.className = "card-tile missing" + (isRemoved ? " marked-remove" : "");
|
||||
tile.innerHTML = `
|
||||
${esc(card.name)}
|
||||
${esc(label)}${hint}
|
||||
<button class="card-overlay-btn btn-remove-card" onclick="toggleRemove(${card.id})">✕</button>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
while (grid.children.length > cards.length) grid.removeChild(grid.lastChild);
|
||||
while (grid.children.length > displayCards.length) grid.removeChild(grid.lastChild);
|
||||
|
||||
// Edit add section
|
||||
let addSection = area.querySelector(".edit-add-section");
|
||||
@@ -319,7 +354,7 @@ function renderCards(slug, cards) {
|
||||
<div class="search-results" id="searchResults"></div>
|
||||
</div>
|
||||
<div class="section-label" style="margin-top:12px">Bulk Add (one per line)</div>
|
||||
<textarea id="bulkAddArea" placeholder="Paste card names..."></textarea>
|
||||
<textarea id="bulkAddArea" placeholder="Card names or Scryfall card URLs — one per line"></textarea>
|
||||
<div class="row"><button class="btn btn-green" onclick="saveEdit()" style="margin-top:4px">Save Changes</button></div>
|
||||
`;
|
||||
area.appendChild(addSection);
|
||||
@@ -369,7 +404,7 @@ async function saveEdit() {
|
||||
const deck = deckData[activeDeck];
|
||||
const body = {
|
||||
deck_name: deck.deck_name,
|
||||
commander: editCommander || deck.commander,
|
||||
commander: deck.commander ? (editCommander || deck.commander) : null,
|
||||
remove: [...editRemovals],
|
||||
add: addNames,
|
||||
};
|
||||
@@ -494,14 +529,16 @@ document.addEventListener("keydown", e => { if (e.key === "Escape") closeLightbo
|
||||
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 || !cards) { err.textContent = "Need a name and card list."; err.style.display = ""; return; }
|
||||
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;
|
||||
try {
|
||||
const r = await api("POST", "/decks", { deck_name: name, cards, commander });
|
||||
const r = await api("POST", "/decks", { deck_name: name, cards, commander, is_tokens: isTokens });
|
||||
clearForm();
|
||||
await loadDecks();
|
||||
selectDeck(r.slug);
|
||||
@@ -511,8 +548,15 @@ async function submitDeck() {
|
||||
} finally { btn.disabled = false; }
|
||||
}
|
||||
|
||||
function onTokenToggle() {
|
||||
const on = document.getElementById("isTokens").checked;
|
||||
document.getElementById("commanderWrap").style.display = on ? "none" : "";
|
||||
}
|
||||
|
||||
function clearForm() {
|
||||
["deckName","deckCommander","cardList"].forEach(id => document.getElementById(id).value = "");
|
||||
document.getElementById("isTokens").checked = false;
|
||||
onTokenToggle();
|
||||
document.getElementById("submitError").style.display = "none";
|
||||
}
|
||||
|
||||
@@ -539,14 +583,35 @@ async function deleteDeck() {
|
||||
} catch (e) { alert(e.message); }
|
||||
}
|
||||
|
||||
function downloadTTS() { if (activeDeck) window.location.href = `/decks/${activeDeck}/tts`; }
|
||||
async function copyTTSUrl() {
|
||||
if (!activeDeck) return;
|
||||
const url = `${window.location.origin}/decks/${activeDeck}/tts.json`;
|
||||
try {
|
||||
await navigator.clipboard.writeText(url);
|
||||
const btn = document.getElementById("dlBtn");
|
||||
const orig = btn.textContent;
|
||||
btn.textContent = "Copied!";
|
||||
setTimeout(() => btn.textContent = orig, 1500);
|
||||
} catch {
|
||||
prompt("Copy this URL:", url);
|
||||
}
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
return String(s).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");
|
||||
}
|
||||
|
||||
function cardLabel(card) {
|
||||
if (card.name) return card.name;
|
||||
if (card.scry_url) {
|
||||
const tail = (card.scry_url.split("/card/")[1] || "").replace(/\/$/, "").split("/");
|
||||
if (tail.length >= 2) return `${tail[0]} · ${tail[1]}`;
|
||||
}
|
||||
return "card";
|
||||
}
|
||||
|
||||
setInterval(loadDecks, 5000);
|
||||
loadDecks();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
Reference in New Issue
Block a user