2026-03-23 16:38:18 -07:00
{% extends "base.html" %}
2026-04-04 19:59:03 -07:00
{% block banner_subtitle %}Upgrade Suggestions{% endblock %}
2026-03-23 16:38:18 -07:00
{% block content %}
2026-04-04 19:59:03 -07:00
< h2 > Upgrade Suggestions< / h2 >
2026-03-23 16:38:18 -07:00
{% 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 %}
{% if error %}
< div class = "panel panel-info-warning" > {{ error }}< / div >
{% elif not budget_report %}
< div class = "panel" > No budget report available for this deck.< / div >
{% else %}
{% set bstatus = budget_report.budget_status %}
< div class = "budget-badge budget-badge--{{ bstatus }}" style = "margin-bottom:.75rem;" >
{% if bstatus == 'under' %}
Under Budget: ${{ "%.2f"|format(budget_report.total_price) }} / ${{ "%.2f"|format(budget_config.total) }}
{% elif bstatus == 'soft_exceeded' %}
Over Budget (soft): ${{ "%.2f"|format(budget_report.total_price) }} / ${{ "%.2f"|format(budget_config.total) }}
(+${{ "%.2f"|format(budget_report.overage) }})
{% else %}
Hard Cap Exceeded: ${{ "%.2f"|format(budget_report.total_price) }} / ${{ "%.2f"|format(budget_config.total) }}
(+${{ "%.2f"|format(budget_report.overage) }})
{% endif %}
< / div >
{% if stale_prices_global is defined and stale_prices_global %}
< div class = "stale-banner" > ⏱ Prices shown may be more than 24 hours old. Refresh price data on the Setup page if you need current values.< / div >
{% endif %}
{% if budget_report.pickups_list %}
< p class = "muted text-sm" style = "margin-bottom:.5rem;" >
2026-04-04 19:59:03 -07:00
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 >
2026-03-23 16:38:18 -07:00
< / p >
2026-04-04 19:59:03 -07:00
{# 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 >
2026-03-23 16:38:18 -07:00
< table class = "pickups-table" style = "width:100%; border-collapse:collapse;" >
< thead >
< tr >
2026-04-04 19:59:03 -07:00
< th class = "cart-cb-th" aria-label = "Select" > < / th >
2026-03-23 16:38:18 -07:00
< th style = "text-align:left; padding:.4rem .5rem; border-bottom:1px solid var(--border,#333);" > Card< / th >
2026-04-04 19:59:03 -07:00
< 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 >
2026-03-23 16:38:18 -07:00
< 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 >
< / thead >
< tbody >
{% for card in budget_report.pickups_list %}
< tr >
2026-04-04 19:59:03 -07:00
< 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 >
2026-03-23 16:38:18 -07:00
< 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);" >
2026-04-04 19:59:03 -07:00
{% 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 %}
2026-03-23 16:38:18 -07:00
${{ "%.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 >
2026-04-04 19:59:03 -07:00
< 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 >
2026-03-23 16:38:18 -07:00
< 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 >
< td style = "text-align:right; padding:.35rem .5rem; border-bottom:1px solid var(--border-subtle,#222);" class = "muted" >
{{ card.priority }}
< / td >
< / tr >
{% endfor %}
< / tbody >
< / table >
2026-04-04 19:59:03 -07:00
< 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 >
2026-03-23 16:38:18 -07:00
{% else %}
2026-04-04 19:59:03 -07:00
< div class = "panel muted" > No upgrade suggestions available — your deck may already fit the budget well.< / div >
2026-03-23 16:38:18 -07:00
{% endif %}
{% if budget_report.over_budget_cards %}
< h3 style = "margin-top:1.25rem;" > Over-Budget Cards< / h3 >
< p class = "muted text-sm" style = "margin-bottom:.5rem;" >
Cards in your current deck that exceed the budget or per-card ceiling.
< / p >
< table style = "width:100%; border-collapse:collapse;" >
< thead >
< tr >
< 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:left; padding:.4rem .5rem; border-bottom:1px solid var(--border,#333);" > Note< / th >
< / tr >
< / thead >
< tbody >
{% for c in budget_report.over_budget_cards %}
< tr >
< td style = "padding:.35rem .5rem; border-bottom:1px solid var(--border-subtle,#222);" > {{ c.card }}< / td >
< td style = "text-align:right; padding:.35rem .5rem; border-bottom:1px solid var(--border-subtle,#222);" > ${{ "%.2f"|format(c.price) }}{% if stale_prices is defined and c.card|lower in stale_prices %}< span class = "stale-price-badge" title = "Price may be outdated (>24h)" > ⏱ < / span > {% endif %}< / td >
< td style = "padding:.35rem .5rem; border-bottom:1px solid var(--border-subtle,#222);" class = "muted" >
{% if c.ceiling_exceeded %}Above ${{ "%.2f"|format(budget_config.card_ceiling) }} ceiling{% endif %}
< / td >
< / tr >
{% endfor %}
< / tbody >
< / table >
{% endif %}
{% endif %}
< div style = "margin-top:1rem;" >
< a href = "/decks/view?name={{ name|urlencode }}" class = "btn" role = "button" > Back to Deck< / a >
< a href = "/decks" class = "btn" role = "button" > All Decks< / a >
< / div >
{% endblock %}
2026-04-04 19:59:03 -07:00