mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-12-24 03:20:12 +01:00
feat(web): Multi-Copy modal earlier; Multi-Copy stage before lands; bump version to 2.1.1; update CHANGELOG\n\n- Modal triggers after commander selection (Step 2)\n- Multi-Copy applied first in Step 5, lands next\n- Keep mc_summary/clamp/adjustments wiring intact\n- Tests green
This commit is contained in:
parent
be672ac5d2
commit
341a216ed3
20 changed files with 1271 additions and 21 deletions
79
code/web/templates/build/_multi_copy_modal.html
Normal file
79
code/web/templates/build/_multi_copy_modal.html
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="mcTitle" style="position:fixed; inset:0; z-index:1000; display:flex; align-items:center; justify-content:center;">
|
||||
<div class="modal-backdrop" style="position:absolute; inset:0; background:rgba(0,0,0,.6);"></div>
|
||||
<div class="modal-content" style="position:relative; max-width:620px; width:clamp(320px, 90vw, 620px); background:#0f1115; border:1px solid var(--border); border-radius:10px; box-shadow:0 10px 30px rgba(0,0,0,.5); padding:1rem;">
|
||||
<div class="modal-header" style="display:flex; align-items:center; justify-content:space-between; gap:.5rem;">
|
||||
<h3 id="mcTitle" style="margin:0;">Consider a multi-copy package?</h3>
|
||||
<button type="button" class="btn" aria-label="Close" onclick="try{this.closest('.modal').remove();}catch(_){ }">×</button>
|
||||
</div>
|
||||
<form hx-post="/build/multicopy/save" hx-target="closest .modal" hx-swap="outerHTML" onsubmit="return validateMultiCopyForm(this);">
|
||||
<fieldset>
|
||||
<legend>Choose one archetype</legend>
|
||||
<div style="display:grid; gap:.5rem;">
|
||||
{% for it in items %}
|
||||
<label class="mc-option" style="display:grid; grid-template-columns: auto 1fr; gap:.5rem; align-items:flex-start; padding:.5rem; border:1px solid var(--border); border-radius:8px; background:#0b0d12;">
|
||||
<input type="radio" name="choice_id" value="{{ it.id }}" {% if loop.first %}checked{% endif %} />
|
||||
<div>
|
||||
<div><strong>{{ it.name }}</strong> {% if it.printed_cap %}<span class="muted">(Cap: {{ it.printed_cap }})</span>{% endif %}</div>
|
||||
{% if it.reasons %}
|
||||
<div class="muted" style="font-size:12px;">Signals: {{ ', '.join(it.reasons) }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset style="margin-top:.5rem;">
|
||||
<legend>How many copies?</legend>
|
||||
{% set first = items[0] %}
|
||||
{% set cap = first.printed_cap %}
|
||||
{% set rec = first.rec_window if first.rec_window else (20,30) %}
|
||||
<div id="mc-count-row" class="mc-count" style="display:flex; align-items:center; gap:.5rem; flex-wrap:wrap;">
|
||||
<input type="number" min="1" name="count" value="{{ first.default_count or 25 }}" />
|
||||
{% if cap %}
|
||||
<small class="muted">Max {{ cap }}</small>
|
||||
{% else %}
|
||||
<small class="muted">Suggested {{ rec[0] }}–{{ rec[1] }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div id="mc-thrum-row" style="margin-top:.35rem;">
|
||||
<label title="Adds 1 copy of Thrumming Stone if applicable.">
|
||||
<input type="checkbox" name="thrumming" value="1" {% if first.thrumming_stone_synergy %}checked{% endif %} /> Include Thrumming Stone
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div class="modal-footer" style="display:flex; gap:.5rem; justify-content:flex-end; margin-top:1rem;">
|
||||
<button type="button" class="btn" onclick="this.closest('.modal').remove()">Cancel</button>
|
||||
<button type="submit" class="btn-continue">Save</button>
|
||||
<button type="submit" class="btn" name="skip" value="1" title="Don't ask again for this commander/theme combo">Skip</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function(){
|
||||
function qs(sel, root){ return (root||document).querySelector(sel); }
|
||||
var modal = document.currentScript && document.currentScript.previousElementSibling ? document.currentScript.previousElementSibling.previousElementSibling : document.querySelector('.modal');
|
||||
var form = modal ? modal.querySelector('form') : null;
|
||||
function updateForChoice(choice){
|
||||
try {
|
||||
var countRow = qs('#mc-count-row', modal);
|
||||
var thrumRow = qs('#mc-thrum-row', modal);
|
||||
if (!choice || !countRow) return;
|
||||
// Server provides only items array; embed metadata via dataset for dynamic hints when switching radio
|
||||
var metaEl = choice.closest('label.mc-option');
|
||||
var printedCap = metaEl && metaEl.querySelector('.muted') && metaEl.querySelector('.muted').textContent.match(/Cap: (\d+)/);
|
||||
var cap = printedCap ? parseInt(printedCap[1], 10) : null;
|
||||
var num = countRow.querySelector('input[name="count"]');
|
||||
if (cap){ num.max = String(cap); if (parseInt(num.value||'0',10) > cap){ num.value = String(cap); } }
|
||||
else { num.removeAttribute('max'); }
|
||||
} catch(_){}
|
||||
}
|
||||
if (form){
|
||||
var radios = form.querySelectorAll('input[name="choice_id"]');
|
||||
Array.prototype.forEach.call(radios, function(r){ r.addEventListener('change', function(){ updateForChoice(r); }); });
|
||||
if (radios.length){ updateForChoice(radios[0]); }
|
||||
}
|
||||
window.validateMultiCopyForm = function(f){ try{ return true; }catch(_){ return true; } };
|
||||
document.addEventListener('keydown', function(e){ if (e.key === 'Escape'){ try{ modal && modal.remove(); }catch(_){ } } });
|
||||
})();
|
||||
</script>
|
||||
|
|
@ -8,6 +8,7 @@
|
|||
</aside>
|
||||
<div class="grow" data-skeleton>
|
||||
<div hx-get="/build/banner" hx-trigger="load"></div>
|
||||
<div hx-get="/build/multicopy/check" hx-trigger="load" hx-swap="afterend"></div>
|
||||
|
||||
<form hx-post="/build/step2" hx-target="#wizard" hx-swap="innerHTML">
|
||||
<input type="hidden" name="commander" value="{{ commander.name }}" />
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@
|
|||
<div class="grow" data-skeleton>
|
||||
<div hx-get="/build/banner" hx-trigger="load"></div>
|
||||
|
||||
<div hx-get="/build/multicopy/check" hx-trigger="load" hx-swap="afterend"></div>
|
||||
|
||||
|
||||
|
||||
{% if error %}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,9 @@
|
|||
<img src="https://api.scryfall.com/cards/named?fuzzy={{ commander|urlencode }}&format=image&version=normal" alt="{{ commander }} card image" data-card-name="{{ commander }}" />
|
||||
</a>
|
||||
</aside>
|
||||
<div class="grow" data-skeleton>
|
||||
<div class="grow" data-skeleton>
|
||||
<div hx-get="/build/banner" hx-trigger="load"></div>
|
||||
<div hx-get="/build/multicopy/check" hx-trigger="load" hx-swap="afterend"></div>
|
||||
{% if locks_restored and locks_restored > 0 %}
|
||||
<div class="muted" style="margin:.35rem 0;">
|
||||
<span class="chip" title="Locks restored from permalink">🔒 {{ locks_restored }} locks restored</span>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
</aside>
|
||||
<div class="grow" data-skeleton>
|
||||
<div hx-get="/build/banner" hx-trigger="load"></div>
|
||||
<div hx-get="/build/multicopy/check" hx-trigger="load" hx-swap="afterend"></div>
|
||||
|
||||
<p>Commander: <strong>{{ commander }}</strong></p>
|
||||
<p>Tags: {{ tags|default([])|join(', ') }}</p>
|
||||
|
|
@ -48,6 +49,12 @@
|
|||
{% if added_total is not none %}
|
||||
<span class="chip"><span class="dot" style="background: var(--blue-main);"></span> Added {{ added_total }}</span>
|
||||
{% endif %}
|
||||
{% if clamped_overflow is defined and clamped_overflow and (clamped_overflow > 0) %}
|
||||
<span class="chip" title="Trimmed overflow from this stage"><span class="dot" style="background: var(--red-main);"></span> Clamped {{ clamped_overflow }}</span>
|
||||
{% endif %}
|
||||
{% if stage_label and stage_label == 'Multi-Copy Package' and mc_summary is defined and mc_summary %}
|
||||
<span class="chip" title="Multi-Copy package summary"><span class="dot" style="background: var(--purple-main);"></span> {{ mc_summary }}</span>
|
||||
{% endif %}
|
||||
<span id="locks-chip">{% if locks and locks|length > 0 %}<span class="chip" title="Locked cards">🔒 {{ locks|length }} locked</span>{% endif %}</span>
|
||||
<button type="button" class="btn" style="margin-left:auto;" title="Copy permalink"
|
||||
onclick="(async()=>{try{const r=await fetch('/build/permalink');const j=await r.json();const url=(j.permalink?location.origin+j.permalink:location.href+'#'+btoa(JSON.stringify(j.state||{}))); await navigator.clipboard.writeText(url); toast && toast('Permalink copied');}catch(e){alert('Copied state to console'); console.log(e);}})()">Copy Permalink</button>
|
||||
|
|
@ -60,6 +67,10 @@
|
|||
<div class="bar"></div>
|
||||
</div>
|
||||
|
||||
{% if mc_adjustments is defined and mc_adjustments and stage_label and stage_label == 'Multi-Copy Package' %}
|
||||
<div class="muted" style="margin:.35rem 0 .25rem 0;">Adjusted targets: {{ mc_adjustments|join(', ') }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if status %}
|
||||
<div style="margin-top:1rem;">
|
||||
<strong>Status:</strong> {{ status }}{% if stage_label %} — <em>{{ stage_label }}</em>{% endif %}
|
||||
|
|
@ -216,7 +227,7 @@
|
|||
sizes="160px" />
|
||||
</button>
|
||||
<div class="owned-badge" title="{{ 'Owned' if owned else 'Not owned' }}" aria-label="{{ 'Owned' if owned else 'Not owned' }}">{% if owned %}✔{% else %}✖{% endif %}</div>
|
||||
<div class="name">{{ c.name }}{% if c.count and c.count > 1 %} ×{{ c.count }}{% endif %}</div>
|
||||
<div class="name">{{ c.name|safe }}{% if c.count and c.count > 1 %} ×{{ c.count }}{% endif %}</div>
|
||||
<div class="lock-box" id="lock-{{ group_idx }}-{{ loop.index0 }}" style="display:flex; justify-content:center; gap:.25rem; margin-top:.25rem;">
|
||||
<button type="button" class="btn-lock" title="{{ 'Unlock this card (kept across reruns)' if is_locked else 'Lock this card (keep across reruns)' }}" aria-pressed="{{ 'true' if is_locked else 'false' }}"
|
||||
hx-post="/build/lock" hx-target="closest .lock-box" hx-swap="innerHTML"
|
||||
|
|
@ -254,7 +265,7 @@
|
|||
sizes="160px" />
|
||||
</button>
|
||||
<div class="owned-badge" title="{{ 'Owned' if owned else 'Not owned' }}" aria-label="{{ 'Owned' if owned else 'Not owned' }}">{% if owned %}✔{% else %}✖{% endif %}</div>
|
||||
<div class="name">{{ c.name }}{% if c.count and c.count > 1 %} ×{{ c.count }}{% endif %}</div>
|
||||
<div class="name">{{ c.name|safe }}{% if c.count and c.count > 1 %} ×{{ c.count }}{% endif %}</div>
|
||||
<div class="lock-box" id="lock-{{ loop.index0 }}" style="display:flex; justify-content:center; gap:.25rem; margin-top:.25rem;">
|
||||
<button type="button" class="btn-lock" title="{{ 'Unlock this card (kept across reruns)' if is_locked else 'Lock this card (keep across reruns)' }}" aria-pressed="{{ 'true' if is_locked else 'false' }}"
|
||||
hx-post="/build/lock" hx-target="closest .lock-box" hx-swap="innerHTML"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue