mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2026-04-06 21:15:20 +02:00
feat: Card Kingdom prices, shopping cart export, and hover panel fixes (#73)
* feat: add CK prices, shopping cart export, and hover panel fixes * fix: include commander in Buy This Deck cart export
This commit is contained in:
parent
dd996939e6
commit
69d84cc414
24 changed files with 899 additions and 146 deletions
|
|
@ -1,7 +1,7 @@
|
|||
{% extends "base.html" %}
|
||||
{% block banner_subtitle %}Budget Pickups{% endblock %}
|
||||
{% block banner_subtitle %}Upgrade Suggestions{% endblock %}
|
||||
{% block content %}
|
||||
<h2>Pickups List</h2>
|
||||
<h2>Upgrade Suggestions</h2>
|
||||
{% if commander %}
|
||||
<div class="muted" style="margin-bottom:.5rem;">Deck: <strong>{{ commander }}</strong>{% if name %} — <span class="muted text-xs">{{ name }}</span>{% endif %}</div>
|
||||
{% endif %}
|
||||
|
|
@ -30,13 +30,35 @@
|
|||
|
||||
{% if budget_report.pickups_list %}
|
||||
<p class="muted text-sm" style="margin-bottom:.5rem;">
|
||||
Cards you don't own yet that fit the deck's themes and budget. Sorted by theme match priority.
|
||||
Cards that fit the deck's themes and budget. Owned cards are free upgrades — just swap one in. Sorted by theme match priority.
|
||||
<span class="price-legend" style="display:block; margin-top:.2rem;">TCG = TCGPlayer · CK = Card Kingdom</span>
|
||||
</p>
|
||||
|
||||
{# Cart toolbar #}
|
||||
<div class="cart-toolbar" id="cart-toolbar">
|
||||
<label style="display:inline-flex;align-items:center;gap:.35rem;cursor:pointer;font-size:.85rem;">
|
||||
<input type="checkbox" id="cart-select-all" checked aria-label="Select all cards">
|
||||
<span>Select all</span>
|
||||
</label>
|
||||
<span id="cart-selected-count" class="cart-label"></span>
|
||||
<button id="btn-copy-tcg" class="btn btn-sm" type="button" onclick="cartCopyTCG()">Open in TCGPlayer</button>
|
||||
<button id="btn-copy-ck" class="btn btn-sm" type="button" onclick="cartCopyCK()">Open in Card Kingdom</button>
|
||||
</div>
|
||||
{# Fallback textarea (shown when Clipboard API is unavailable) #}
|
||||
<div id="cart-fallback-wrap" class="cart-fallback-wrap" style="display:none;">
|
||||
<p>Clipboard access unavailable. Select all and copy the text below:</p>
|
||||
<textarea id="cart-fallback-text" class="cart-fallback-textarea" readonly aria-label="Card list for manual copy"></textarea>
|
||||
<button type="button" class="btn btn-sm" style="margin-top:.4rem;" onclick="document.getElementById('cart-fallback-wrap').style.display='none';">Close</button>
|
||||
</div>
|
||||
|
||||
<table class="pickups-table" style="width:100%; border-collapse:collapse;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="cart-cb-th" aria-label="Select"></th>
|
||||
<th style="text-align:left; padding:.4rem .5rem; border-bottom:1px solid var(--border,#333);">Card</th>
|
||||
<th style="text-align:right; padding:.4rem .5rem; border-bottom:1px solid var(--border,#333);">Price</th>
|
||||
<th style="text-align:right; padding:.4rem .5rem; border-bottom:1px solid var(--border,#333);">TCG</th>
|
||||
<th style="text-align:right; padding:.4rem .5rem; border-bottom:1px solid var(--border,#333);">CK</th>
|
||||
<th style="text-align:center; padding:.4rem .5rem; border-bottom:1px solid var(--border,#333);">Owned</th>
|
||||
<th style="text-align:center; padding:.4rem .5rem; border-bottom:1px solid var(--border,#333);">Tier</th>
|
||||
<th style="text-align:right; padding:.4rem .5rem; border-bottom:1px solid var(--border,#333);">Priority</th>
|
||||
</tr>
|
||||
|
|
@ -44,16 +66,33 @@
|
|||
<tbody>
|
||||
{% for card in budget_report.pickups_list %}
|
||||
<tr>
|
||||
<td class="cart-cb-td">
|
||||
<input type="checkbox" class="cart-cb" data-card-name="{{ card.card|e }}" checked aria-label="Select {{ card.card|e }}">
|
||||
</td>
|
||||
<td style="padding:.35rem .5rem; border-bottom:1px solid var(--border-subtle,#222);">
|
||||
<span class="card-name-price-hover" data-card-name="{{ card.card|e }}">{{ card.card }}</span>
|
||||
</td>
|
||||
<td style="text-align:right; padding:.35rem .5rem; border-bottom:1px solid var(--border-subtle,#222);">
|
||||
{% if card.price is not none %}
|
||||
{% if owned_names is defined and card.card|lower in owned_names %}
|
||||
<span class="muted text-sm">owned</span>
|
||||
{% elif card.price is not none %}
|
||||
${{ "%.2f"|format(card.price) }}{% if stale_prices is defined and card.card|lower in stale_prices %}<span class="stale-price-badge" title="Price may be outdated (>24h)">⏱</span>{% endif %}
|
||||
{% else %}
|
||||
<span class="muted">–</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="text-align:right; padding:.35rem .5rem; border-bottom:1px solid var(--border-subtle,#222);">
|
||||
{% if card.ck_price is not none %}
|
||||
${{ "%.2f"|format(card.ck_price) }}
|
||||
{% else %}
|
||||
<span class="muted">–</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="text-align:center; padding:.35rem .5rem; border-bottom:1px solid var(--border-subtle,#222);">
|
||||
{% if owned_names is defined and card.card|lower in owned_names %}
|
||||
<span class="owned-badge" title="You own this card">yes</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="text-align:center; padding:.35rem .5rem; border-bottom:1px solid var(--border-subtle,#222);">
|
||||
<span class="tier-badge tier-badge--{{ card.tier|lower }}">{{ card.tier }}</span>
|
||||
</td>
|
||||
|
|
@ -64,8 +103,84 @@
|
|||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var allCb = document.getElementById('cart-select-all');
|
||||
|
||||
function getCheckedNames() {
|
||||
return Array.from(document.querySelectorAll('.cart-cb:checked'))
|
||||
.map(function (cb) { return cb.getAttribute('data-card-name'); });
|
||||
}
|
||||
|
||||
function updateState() {
|
||||
var all = document.querySelectorAll('.cart-cb');
|
||||
var checked = document.querySelectorAll('.cart-cb:checked');
|
||||
var n = checked.length, total = all.length;
|
||||
var countEl = document.getElementById('cart-selected-count');
|
||||
if (countEl) countEl.textContent = n + ' of ' + total + ' selected';
|
||||
var btnTCG = document.getElementById('btn-copy-tcg');
|
||||
var btnCK = document.getElementById('btn-copy-ck');
|
||||
if (btnTCG) btnTCG.disabled = n === 0;
|
||||
if (btnCK) btnCK.disabled = n === 0;
|
||||
if (allCb) {
|
||||
allCb.indeterminate = n > 0 && n < total;
|
||||
allCb.checked = n === total;
|
||||
}
|
||||
}
|
||||
|
||||
if (allCb) {
|
||||
allCb.addEventListener('change', function () {
|
||||
document.querySelectorAll('.cart-cb').forEach(function (cb) { cb.checked = allCb.checked; });
|
||||
updateState();
|
||||
});
|
||||
}
|
||||
document.querySelectorAll('.cart-cb').forEach(function (cb) {
|
||||
cb.addEventListener('change', updateState);
|
||||
});
|
||||
|
||||
function stripDFC(n) { return n.split(' // ')[0].trim(); }
|
||||
|
||||
function showFallback(text) {
|
||||
var wrap = document.getElementById('cart-fallback-wrap');
|
||||
var ta = document.getElementById('cart-fallback-text');
|
||||
if (!wrap || !ta) return;
|
||||
ta.value = text;
|
||||
wrap.style.display = 'block';
|
||||
ta.focus();
|
||||
ta.select();
|
||||
}
|
||||
|
||||
function showCartToast(msg) {
|
||||
var el = document.createElement('div');
|
||||
el.className = 'cart-toast-top';
|
||||
el.textContent = msg;
|
||||
document.body.appendChild(el);
|
||||
setTimeout(function () { el.classList.add('hide'); setTimeout(function () { el.remove(); }, 300); }, 6000);
|
||||
}
|
||||
|
||||
function openAfterCopy(url, vendorName) {
|
||||
var names = getCheckedNames();
|
||||
if (!names.length) return;
|
||||
var text = names.map(function (n) { return '1 ' + stripDFC(n); }).join('\n');
|
||||
function doOpen() {
|
||||
showCartToast('List copied — paste into ' + vendorName + ' with Ctrl+V');
|
||||
setTimeout(function () { window.open(url, '_blank'); }, 400);
|
||||
}
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).then(doOpen).catch(function () { showFallback(text); window.open(url, '_blank'); });
|
||||
} else { showFallback(text); window.open(url, '_blank'); }
|
||||
}
|
||||
|
||||
window.cartCopyTCG = function () { openAfterCopy('https://www.tcgplayer.com/massentry', 'TCGPlayer'); };
|
||||
window.cartCopyCK = function () { openAfterCopy('https://www.cardkingdom.com/builder', 'Card Kingdom'); };
|
||||
|
||||
updateState();
|
||||
})();
|
||||
</script>
|
||||
|
||||
{% else %}
|
||||
<div class="panel muted">No pickups suggestions available — your deck may already fit the budget well.</div>
|
||||
<div class="panel muted">No upgrade suggestions available — your deck may already fit the budget well.</div>
|
||||
{% endif %}
|
||||
|
||||
{% if budget_report.over_budget_cards %}
|
||||
|
|
@ -102,3 +217,4 @@
|
|||
<a href="/decks" class="btn" role="button">All Decks</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@
|
|||
{% if commander_overlap_tags %}data-overlaps="{{ commander_overlap_tags|join(', ') }}"{% endif %}
|
||||
{% if commander_reason_text %}data-reasons="{{ commander_reason_text|e }}"{% endif %}
|
||||
width="320" />
|
||||
{# Price overlay — ensures commander price is loaded into window._priceNum for the hover panel #}
|
||||
<div class="card-price-overlay" data-price-for="{{ commander_base }}" aria-hidden="true"></div>
|
||||
</div>
|
||||
<div class="muted" style="margin-top:.25rem;">Commander: <span data-card-name="{{ commander }}"
|
||||
data-original-name="{{ commander }}"
|
||||
|
|
@ -69,12 +71,74 @@
|
|||
{% endif %}
|
||||
<a href="/decks/compare?A={{ name|urlencode }}" class="btn" role="button" title="Compare this deck with another">Compare…</a>
|
||||
{% if budget_report %}
|
||||
<a href="/decks/pickups?name={{ name|urlencode }}" class="btn" role="button" title="View cards to acquire for this budget build">Pickups List</a>
|
||||
<a href="/decks/pickups?name={{ name|urlencode }}" class="btn" role="button" title="View upgrade suggestions for this deck">Upgrade Suggestions</a>
|
||||
{% endif %}
|
||||
<form method="get" action="/decks" style="display:inline; margin:0;">
|
||||
<button type="submit">Back to Finished Decks</button>
|
||||
</form>
|
||||
</div>
|
||||
{# Buy This Deck: collect all cards, strip DFC names, open vendor + copy to clipboard #}
|
||||
{%- set _buy_cards = [] -%}
|
||||
{%- if commander_base -%}
|
||||
{%- set _ = _buy_cards.append({'name': commander_base, 'count': 1}) -%}
|
||||
{%- endif -%}
|
||||
{%- if summary and summary.type_breakdown and summary.type_breakdown.cards -%}
|
||||
{%- for _btype, _bclist in summary.type_breakdown.cards.items() -%}
|
||||
{%- for _bc in _bclist -%}
|
||||
{%- if _bc.name -%}
|
||||
{%- set _ = _buy_cards.append({'name': _bc.name, 'count': (_bc.count if _bc.count and _bc.count > 1 else 1)}) -%}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{%- endfor -%}
|
||||
{%- endif -%}
|
||||
{% if _buy_cards %}
|
||||
<div class="cart-toolbar" style="margin-top:.6rem; flex-direction:column; align-items:flex-start; gap:.35rem;" id="buy-deck-toolbar">
|
||||
<span class="cart-label" style="font-weight:600; color:var(--text,#e5e7eb);">Buy this deck:</span>
|
||||
<div style="display:flex; gap:.4rem; flex-wrap:wrap;">
|
||||
<button class="btn btn-sm" type="button" onclick="buyViaTCG()">Open in TCGPlayer</button>
|
||||
<button class="btn btn-sm" type="button" onclick="buyViaCK()">Open in Card Kingdom</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="buy-fallback-wrap" class="cart-fallback-wrap" style="display:none;">
|
||||
<p>Clipboard access unavailable. Select all and copy the text below, then paste it at the vendor site:</p>
|
||||
<textarea id="buy-fallback-text" class="cart-fallback-textarea" readonly aria-label="Deck list for manual copy"></textarea>
|
||||
<button type="button" class="btn btn-sm" style="margin-top:.4rem;" onclick="document.getElementById('buy-fallback-wrap').style.display='none';">Close</button>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
var _buyCards = {{ _buy_cards | tojson }};
|
||||
function stripDFC(n) { return n.split(' // ')[0].trim(); }
|
||||
function buildList(cards) {
|
||||
return cards.map(function (c) { return c.count + ' ' + stripDFC(c.name); }).join('\n');
|
||||
}
|
||||
function showFallback(text) {
|
||||
var wrap = document.getElementById('buy-fallback-wrap');
|
||||
var ta = document.getElementById('buy-fallback-text');
|
||||
if (!wrap || !ta) return;
|
||||
ta.value = text; wrap.style.display = 'block'; ta.focus(); ta.select();
|
||||
}
|
||||
function showCartToast(msg) {
|
||||
var el = document.createElement('div');
|
||||
el.className = 'cart-toast-top';
|
||||
el.textContent = msg;
|
||||
document.body.appendChild(el);
|
||||
setTimeout(function () { el.classList.add('hide'); setTimeout(function () { el.remove(); }, 300); }, 6000);
|
||||
}
|
||||
function openAfterCopy(url, vendorName) {
|
||||
var text = buildList(_buyCards);
|
||||
function doOpen() {
|
||||
showCartToast('List copied — paste into ' + vendorName + ' with Ctrl+V');
|
||||
setTimeout(function () { window.open(url, '_blank'); }, 400);
|
||||
}
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).then(doOpen).catch(function () { showFallback(text); window.open(url, '_blank'); });
|
||||
} else { showFallback(text); window.open(url, '_blank'); }
|
||||
}
|
||||
window.buyViaTCG = function () { openAfterCopy('https://www.tcgplayer.com/massentry', 'TCGPlayer'); };
|
||||
window.buyViaCK = function () { openAfterCopy('https://www.cardkingdom.com/builder', 'Card Kingdom'); };
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
{% if budget_report %}
|
||||
{% set bstatus = budget_report.budget_status %}
|
||||
<div class="budget-badge budget-badge--{{ bstatus }}" style="margin-top:.6rem;">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue