mtg_python_deckbuilder/code/web/templates/decks/view.html
mwisnowski 69d84cc414
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
2026-04-04 19:59:03 -07:00

259 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% block banner_subtitle %}Finished Decks{% endblock %}
{% block content %}
<h2>Finished Deck</h2>
{% if display_name %}
<div><strong>{{ display_name }}</strong></div>
{% endif %}
{% set hover_tags_source = deck_theme_tags if deck_theme_tags else (tags if tags else commander_combined_tags) %}
{% set hover_tags_joined = hover_tags_source|join(', ') %}
<div class="muted">Commander:
<strong class="commander-hover"
data-card-name="{{ commander }}"
data-original-name="{{ commander }}"
data-role="{{ commander_role_label }}"
{% if hover_tags_joined %}data-tags="{{ hover_tags_joined }}"{% endif %}
{% if commander_tag_slugs %}data-tags-slug="{{ commander_tag_slugs|join(' ') }}"{% endif %}
{% if commander_overlap_tags %}data-overlaps="{{ commander_overlap_tags|join(', ') }}"{% endif %}
{% if commander_reason_text %}data-reasons="{{ commander_reason_text|e }}"{% endif %}>{{ commander }}</strong>
{% if tags and tags|length %} • Themes: {{ tags|join(', ') }}{% endif %}
</div>
<div class="muted">This view mirrors the end-of-build summary. Use the buttons to download the CSV/TXT exports.</div>
<div class="two-col two-col-left-rail" style="margin-top:.75rem;">
<aside class="card-preview">
{% if commander %}
{# Strip synergy annotation for Scryfall search and image fuzzy param #}
{% set commander_base = (commander.split(' - Synergy (')[0] if ' - Synergy (' in commander else commander) %}
<div class="commander-card"
tabindex="0"
style="display:inline-block; cursor:pointer;"
data-card-name="{{ commander_base }}"
data-original-name="{{ commander }}"
data-role="{{ commander_role_label }}"
{% if hover_tags_joined %}data-tags="{{ hover_tags_joined }}"{% endif %}
{% if commander_tag_slugs %}data-tags-slug="{{ commander_tag_slugs|join(' ') }}"{% endif %}
{% if commander_overlap_tags %}data-overlaps="{{ commander_overlap_tags|join(', ') }}"{% endif %}
{% if commander_reason_text %}data-reasons="{{ commander_reason_text|e }}"{% endif %}>
<img src="{{ commander_base|card_image('normal') }}"
alt="{{ commander }} card image"
data-card-name="{{ commander_base }}"
data-original-name="{{ commander }}"
data-role="{{ commander_role_label }}"
{% if hover_tags_joined %}data-tags="{{ hover_tags_joined }}"{% endif %}
{% if commander_tag_slugs %}data-tags-slug="{{ commander_tag_slugs|join(' ') }}"{% endif %}
{% 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 }}"
data-role="{{ commander_role_label }}"
{% if hover_tags_joined %}data-tags="{{ hover_tags_joined }}"{% endif %}
{% if commander_tag_slugs %}data-tags-slug="{{ commander_tag_slugs|join(' ') }}"{% endif %}
{% if commander_overlap_tags %}data-overlaps="{{ commander_overlap_tags|join(', ') }}"{% endif %}
{% if commander_reason_text %}data-reasons="{{ commander_reason_text|e }}"{% endif %}>{{ commander }}</span></div>
{% endif %}
<div style="margin-top:.75rem; display:flex; gap:.35rem; flex-wrap:wrap;">
{% if csv_path %}
<form action="/decks/download-csv" method="get" target="_blank" style="display:inline; margin:0;">
<input type="hidden" name="name" value="{{ name }}" />
<button type="submit">Download CSV</button>
</form>
{% endif %}
{% if txt_path %}
<form action="/files" method="get" target="_blank" style="display:inline; margin:0;">
<input type="hidden" name="path" value="{{ txt_path }}" />
<button type="submit">Download TXT</button>
</form>
{% 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 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;">
{% 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 budget_report.over_budget_cards %}
<div class="panel panel-info-warning" style="margin-top:.5rem;">
<strong>Cards over budget:</strong>
<ul class="muted" style="margin:.25rem 0 0 1rem; padding:0; font-size:.85em;">
{% for c in budget_report.over_budget_cards %}
<li>{{ c.card }} — ${{ "%.2f"|format(c.price) }}{% if c.ceiling_exceeded %} (above ${{ "%.2f"|format(budget_config.card_ceiling) }} ceiling){% endif %}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endif %}
</aside>
<div class="grow">
{% if summary %}
{% if owned_set %}
{% set ns = namespace(owned=0, total=0) %}
{% set tb = summary.type_breakdown %}
{% if tb and tb.cards %}
{% for t, clist in tb.cards.items() %}
{% for c in clist %}
{% set cnt = c.count if c.count else 1 %}
{% set ns.total = ns.total + cnt %}
{% if c.name and (c.name|lower in owned_set) %}
{% set ns.owned = ns.owned + cnt %}
{% endif %}
{% endfor %}
{% endfor %}
{% endif %}
{% set not_owned = (ns.total - ns.owned) %}
{% set pct = ( (ns.owned * 100.0 / (ns.total or 1)) | round(1) ) %}
<div class="panel" style="margin-bottom:.75rem;">
<div style="display:flex; gap:1rem; align-items:center; flex-wrap:wrap;">
<div><strong>Ownership</strong></div>
<div class="muted">Owned: {{ ns.owned }} • Not owned: {{ not_owned }} • Total: {{ ns.total }} ({{ pct }}%)</div>
</div>
</div>
{% endif %}
{{ render_cached('partials/deck_summary.html', name, request=request, summary=summary, game_changers=game_changers, owned_set=owned_set, combos=combos, synergies=synergies, versions=versions) | safe }}
{# M8: Price charts accordion — placed in main area, only available on the saved deck view #}
{% if (price_category_chart and price_category_chart.total > 0) or price_histogram_chart %}
<section class="summary-section-lg">
<details class="analytics-accordion" id="price-charts-accordion">
<summary class="combo-summary">
<span>Price Breakdown</span>
<span class="muted text-xs font-normal ml-2">spend by category &amp; distribution</span>
</summary>
<div class="analytics-content mt-3">
{% if price_category_chart and price_category_chart.total > 0 %}
<div class="price-cat-section">
<div class="price-cat-heading">Spend by Category — ${{ '%.2f'|format(price_category_chart.total) }} total</div>
<div class="price-cat-bar" title="Total: ${{ '%.2f'|format(price_category_chart.total) }}">
{% for cat in price_category_chart.order %}
{% set cat_total = price_category_chart.totals.get(cat, 0) %}
{% if cat_total > 0 %}
{% set pct = (cat_total * 100 / price_category_chart.total) | round(1) %}
<div class="price-cat-seg"
style="width:{{ pct }}%; background:{{ price_category_chart.colors.get(cat, '#f59e0b') }};"
title="{{ cat }}: ${{ '%.2f'|format(cat_total) }} ({{ pct }}%)"></div>
{% endif %}
{% endfor %}
</div>
<div class="price-cat-legend">
{% for cat in price_category_chart.order %}
{% set cat_total = price_category_chart.totals.get(cat, 0) %}
{% if cat_total > 0 %}
<span class="price-cat-legend-item">
<span class="price-cat-swatch" style="background:{{ price_category_chart.colors.get(cat, '#f59e0b') }};"></span>
{{ cat }} ${{ '%.2f'|format(cat_total) }}
</span>
{% endif %}
{% endfor %}
</div>
</div>
{% endif %}
{% if price_histogram_chart %}
<div class="price-hist-section">
<div class="price-hist-heading">Price Distribution</div>
<div class="price-hist-bars">
{% for bin in price_histogram_chart %}
<div class="price-hist-column"
data-type="hist"
data-range="${{ '%.2f'|format(bin.range_min) }}${{ '%.2f'|format(bin.range_max) }}"
data-val="{{ bin.count }}"
data-cards="{% for c in bin.cards %}{{ c.name }}|{{ '%.2f'|format(c.price) }}{% if not loop.last %} • {% endif %}{% endfor %}">
<div class="price-hist-bar" style="height:{{ bin.pct | default(0) }}%; background:{{ bin.color }};"></div>
</div>
{% endfor %}
</div>
<div class="price-hist-xlabels">
{% for bin in price_histogram_chart %}
<div class="price-hist-xlabel">{{ bin.x_label }}</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
</details>
</section>
{% endif %}
{% else %}
<div class="muted">No summary available.</div>
{% endif %}
</div>
</div>
{% endblock %}