mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-12-22 02:20:13 +01:00
Web UI: setup progress + logs folding, Finished Decks library, commander search UX (debounce, keyboard, highlights, color chips), ranking fixes (first-word priority, substring include), optional auto-select; setup start reliability (POST+GET), force runs, status with percent/ETA/timestamps; stepwise builder with added stage reporting and sidecar summaries; keyboard grid wrap-around; restrict commander search to eligible rows
This commit is contained in:
parent
8fa040a05a
commit
0f73a85a4e
43 changed files with 4515 additions and 105 deletions
1
code/web/templates/build/_banner_subtitle.html
Normal file
1
code/web/templates/build/_banner_subtitle.html
Normal file
|
|
@ -0,0 +1 @@
|
|||
<div id="banner-status" hx-swap-oob="true">{% if step %}<span class="muted">{{ step }}{% if i is not none and n is not none %} ({{ i }}/{{ n }}){% endif %}</span>: {% endif %}{% if commander %}<strong>{{ commander }}</strong>{% endif %}{% if tags and tags|length > 0 %} - {{ tags|join(', ') }}{% endif %}</div>
|
||||
214
code/web/templates/build/_step1.html
Normal file
214
code/web/templates/build/_step1.html
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
<section>
|
||||
<h3>Step 1: Choose a Commander</h3>
|
||||
|
||||
<form id="cmdr-search-form" hx-post="/build/step1" hx-target="#wizard" hx-swap="innerHTML">
|
||||
<label>Search by name</label>
|
||||
<input id="cmdr-search" type="text" name="query" value="{{ query or '' }}" autocomplete="off" />
|
||||
<button type="submit">Search</button>
|
||||
<label style="margin-left:.5rem; font-weight:normal;">
|
||||
<input type="checkbox" name="auto" value="1" {% if auto %}checked{% endif %} /> Auto-select top match (very confident)
|
||||
</label>
|
||||
</form>
|
||||
<div class="muted" style="margin:.35rem 0 .5rem 0; font-size:.9rem;">
|
||||
Tip: Press Enter to select the highlighted result, or use Up/Down to navigate. If your query is a full first word (e.g., "vivi"), exact first-word matches are prioritized.
|
||||
</div>
|
||||
|
||||
{% if candidates %}
|
||||
<h4>Top matches</h4>
|
||||
<div class="candidate-grid" id="candidate-grid">
|
||||
{% for name, score, colors in candidates %}
|
||||
<div class="candidate-tile" data-card-name="{{ name }}">
|
||||
<form hx-post="/build/step1/confirm" hx-target="#wizard" hx-swap="innerHTML">
|
||||
<input type="hidden" name="name" value="{{ name }}" />
|
||||
<button class="img-btn" type="submit" title="Select {{ name }} (score {{ score }})">
|
||||
<img src="https://api.scryfall.com/cards/named?fuzzy={{ name|urlencode }}&format=image&version=normal"
|
||||
alt="{{ name }}" />
|
||||
</button>
|
||||
</form>
|
||||
<div class="meta">
|
||||
<div class="name"><span class="name-text">{{ name }}</span></div>
|
||||
<div class="score">
|
||||
match {{ score }}%
|
||||
{% if colors %}
|
||||
<span class="colors" style="margin-left:.25rem;">
|
||||
{% for c in colors %}
|
||||
<span class="chip chip-{{ c|lower }}" title="{{ c }}">{{ c }}</span>
|
||||
{% endfor %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<form hx-post="/build/step1/inspect" hx-target="#wizard" hx-swap="innerHTML" style="margin-top:.25rem;">
|
||||
<input type="hidden" name="name" value="{{ name }}" />
|
||||
<button type="submit">Inspect</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if inspect and inspect.ok %}
|
||||
<div class="two-col two-col-left-rail">
|
||||
<aside class="card-preview card-sm" data-card-name="{{ selected }}">
|
||||
<a href="https://scryfall.com/search?q={{ selected|urlencode }}" target="_blank" rel="noopener">
|
||||
<img src="https://api.scryfall.com/cards/named?fuzzy={{ selected|urlencode }}&format=image&version=normal" alt="{{ selected }} card image" />
|
||||
</a>
|
||||
</aside>
|
||||
<div class="grow">
|
||||
<h4>Theme Tags</h4>
|
||||
{% if tags and tags|length > 0 %}
|
||||
<ul>
|
||||
{% for t in tags %}
|
||||
<li>{{ t }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="muted">No theme tags found for this commander.</p>
|
||||
{% endif %}
|
||||
<div style="margin-top:.75rem;">
|
||||
<form style="display:inline" hx-post="/build/step1/confirm" hx-target="#wizard" hx-swap="innerHTML">
|
||||
<input type="hidden" name="name" value="{{ selected }}" />
|
||||
<button>Use this commander</button>
|
||||
</form>
|
||||
<form style="display:inline" hx-post="/build/step1" hx-target="#wizard" hx-swap="innerHTML">
|
||||
<input type="hidden" name="query" value="" />
|
||||
<button>Back to search</button>
|
||||
</form>
|
||||
<form action="/build" method="get" style="display:inline; margin-left:.5rem;">
|
||||
<button type="submit">Start over</button>
|
||||
</form>
|
||||
</div>
|
||||
<div hx-get="/build/banner?step=Choose%20Commander&i=1&n=5" hx-trigger="load"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% elif inspect and not inspect.ok %}
|
||||
<div style="color:#a00">{{ inspect.error }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if error %}
|
||||
<div style="color:#a00">{{ error }}</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
<script>
|
||||
(function(){
|
||||
var input = document.getElementById('cmdr-search');
|
||||
var form = document.getElementById('cmdr-search-form');
|
||||
var grid = document.getElementById('candidate-grid');
|
||||
if (!input || !form) return;
|
||||
// Debounce live search
|
||||
var t = null;
|
||||
function submit(){
|
||||
if (!form) return;
|
||||
// Trigger the HTMX post without clicking submit
|
||||
if (window.htmx) { window.htmx.trigger(form, 'submit'); }
|
||||
else { form.submit(); }
|
||||
}
|
||||
input.addEventListener('input', function(){
|
||||
if (t) clearTimeout(t);
|
||||
t = setTimeout(submit, 250);
|
||||
});
|
||||
// Keyboard navigation: up/down to move selection, Enter to choose/inspect
|
||||
document.addEventListener('keydown', function(e){
|
||||
if (!grid || !grid.children || grid.children.length === 0) return;
|
||||
var tiles = Array.prototype.slice.call(grid.querySelectorAll('.candidate-tile'));
|
||||
// Ensure something is selected by default
|
||||
var idx = tiles.findIndex(function(el){ return el.classList.contains('active'); });
|
||||
if (idx < 0 && tiles.length > 0) {
|
||||
tiles[0].classList.add('active');
|
||||
idx = 0;
|
||||
}
|
||||
|
||||
// Determine columns via first row's offsetTop count
|
||||
var cols = 1;
|
||||
if (tiles.length > 1) {
|
||||
var firstTop = tiles[0].offsetTop;
|
||||
cols = tiles.findIndex(function(el, i){ return i>0 && el.offsetTop !== firstTop; });
|
||||
if (cols === -1 || cols === 0) cols = tiles.length; // single row fallback
|
||||
}
|
||||
|
||||
function setActive(newIdx) {
|
||||
// Clamp to bounds; wrapping handled by callers
|
||||
newIdx = Math.max(0, Math.min(tiles.length - 1, newIdx));
|
||||
tiles.forEach(function(el){ el.classList.remove('active'); });
|
||||
tiles[newIdx].classList.add('active');
|
||||
tiles[newIdx].scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
||||
return newIdx;
|
||||
}
|
||||
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
var total = tiles.length;
|
||||
var rows = Math.ceil(total / cols);
|
||||
var row = Math.floor(idx / cols);
|
||||
var col = idx % cols;
|
||||
var newRow = row + 1;
|
||||
if (newRow >= rows) newRow = 0; // wrap to first row
|
||||
var newIdx = newRow * cols + col;
|
||||
if (newIdx >= total) newIdx = total - 1; // clamp to last tile if last row shorter
|
||||
idx = setActive(newIdx);
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
var totalU = tiles.length;
|
||||
var rowsU = Math.ceil(totalU / cols);
|
||||
var rowU = Math.floor(idx / cols);
|
||||
var colU = idx % cols;
|
||||
var newRowU = rowU - 1;
|
||||
if (newRowU < 0) newRowU = rowsU - 1; // wrap to last row
|
||||
var newIdxU = newRowU * cols + colU;
|
||||
if (newIdxU >= totalU) newIdxU = totalU - 1;
|
||||
idx = setActive(newIdxU);
|
||||
} else if (e.key === 'ArrowRight') {
|
||||
e.preventDefault();
|
||||
var totalR = tiles.length;
|
||||
idx = setActive((idx + 1) % totalR);
|
||||
} else if (e.key === 'ArrowLeft') {
|
||||
e.preventDefault();
|
||||
var totalL = tiles.length;
|
||||
idx = setActive((idx - 1 + totalL) % totalL);
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
var active = tiles[idx];
|
||||
if (!active) return;
|
||||
var formSel = active.querySelector('form[hx-post="/build/step1/confirm"]');
|
||||
if (formSel) {
|
||||
if (window.htmx) { window.htmx.trigger(formSel, 'submit'); }
|
||||
else if (formSel.submit) { formSel.submit(); }
|
||||
else {
|
||||
var btn = active.querySelector('button');
|
||||
if (btn) btn.click();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
// Highlight matched text
|
||||
try {
|
||||
var q = (input.value || '').trim().toLowerCase();
|
||||
if (q && grid) {
|
||||
grid.querySelectorAll('.name-text').forEach(function(el){
|
||||
var txt = el.textContent || '';
|
||||
var low = txt.toLowerCase();
|
||||
var i = low.indexOf(q);
|
||||
if (i >= 0) {
|
||||
el.innerHTML = txt.substring(0, i) + '<mark>' + txt.substring(i, i+q.length) + '</mark>' + txt.substring(i+q.length);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch(_){}
|
||||
})();
|
||||
</script>
|
||||
<style>
|
||||
.candidate-grid { display:grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap:.75rem; }
|
||||
.candidate-tile { border:1px solid var(--border); border-radius:8px; background:#0f1115; padding:.5rem; }
|
||||
.candidate-tile.active { outline:2px solid #3b82f6; }
|
||||
.img-btn { display:block; width:100%; background:transparent; border:0; padding:0; cursor:pointer; }
|
||||
.img-btn img { width:100%; height:auto; border-radius:6px; }
|
||||
.chip { display:inline-block; padding:0 .35rem; border-radius:999px; font-size:.75rem; line-height:1.4; border:1px solid var(--border); background:#151821; margin-left:.15rem; }
|
||||
.chip-w { background:#fdf4d6; color:#6b4f00; border-color:#e9d8a6; }
|
||||
.chip-u { background:#dbeafe; color:#1e40af; border-color:#93c5fd; }
|
||||
.chip-b { background:#e5e7eb; color:#111827; border-color:#9ca3af; }
|
||||
.chip-g { background:#dcfce7; color:#065f46; border-color:#86efac; }
|
||||
.chip-r { background:#fee2e2; color:#991b1b; border-color:#fecaca; }
|
||||
.chip-c { background:#f3f4f6; color:#111827; border-color:#e5e7eb; }
|
||||
mark { background: rgba(251, 191, 36, .35); color: inherit; padding:0 .1rem; border-radius:2px; }
|
||||
.candidate-tile { cursor: pointer; }
|
||||
</style>
|
||||
77
code/web/templates/build/_step2.html
Normal file
77
code/web/templates/build/_step2.html
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
<section>
|
||||
<h3>Step 2: Tags & Bracket</h3>
|
||||
<div class="two-col two-col-left-rail">
|
||||
<aside class="card-preview" data-card-name="{{ commander.name }}">
|
||||
<a href="https://scryfall.com/search?q={{ commander.name|urlencode }}" target="_blank" rel="noopener">
|
||||
<img src="https://api.scryfall.com/cards/named?fuzzy={{ commander.name|urlencode }}&format=image&version=normal" alt="{{ commander.name }} card image" />
|
||||
</a>
|
||||
</aside>
|
||||
<div class="grow">
|
||||
<div hx-get="/build/banner?step=Tags%20%26%20Bracket&i=2&n=5" hx-trigger="load"></div>
|
||||
|
||||
<form hx-post="/build/step2" hx-target="#wizard" hx-swap="innerHTML">
|
||||
<input type="hidden" name="commander" value="{{ commander.name }}" />
|
||||
{% if error %}
|
||||
<div style="color:#a00; margin:.5rem 0;">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
<fieldset>
|
||||
<legend>Theme Tags</legend>
|
||||
{% if tags %}
|
||||
<label>Primary
|
||||
<select name="primary_tag">
|
||||
<option value="">-- none --</option>
|
||||
{% for t in tags %}
|
||||
<option value="{{ t }}" {% if t == primary_tag %}selected{% endif %}>{{ t }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label>Secondary
|
||||
<select name="secondary_tag">
|
||||
<option value="">-- none --</option>
|
||||
{% for t in tags %}
|
||||
<option value="{{ t }}" {% if t == secondary_tag %}selected{% endif %}>{{ t }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label>Tertiary
|
||||
<select name="tertiary_tag">
|
||||
<option value="">-- none --</option>
|
||||
{% for t in tags %}
|
||||
<option value="{{ t }}" {% if t == tertiary_tag %}selected{% endif %}>{{ t }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
{% else %}
|
||||
<p>No theme tags available for this commander.</p>
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Budget/Power Bracket</legend>
|
||||
<div style="display:grid; gap:.5rem;">
|
||||
{% for b in brackets %}
|
||||
<label style="display:flex; gap:.5rem; align-items:flex-start;">
|
||||
<input type="radio" name="bracket" value="{{ b.level }}" {% if (selected_bracket is defined and selected_bracket == b.level) or (selected_bracket is not defined and loop.first) %}checked{% endif %} />
|
||||
<span><strong>{{ b.name }}</strong> — <small>{{ b.desc }}</small></span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="muted" style="margin-top:.35rem; font-size:.9em;">
|
||||
Note: This guides deck creation and relaxes/raises constraints, but it is not a guarantee the final deck strictly fits that bracket.
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div style="margin-top:1rem;">
|
||||
<button type="submit">Continue to Ideals</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div style="margin-top:.5rem;">
|
||||
<form action="/build" method="get" style="display:inline; margin:0;">
|
||||
<button type="submit">Start over</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
44
code/web/templates/build/_step3.html
Normal file
44
code/web/templates/build/_step3.html
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<section>
|
||||
<h3>Step 3: Ideal Counts</h3>
|
||||
<div class="two-col two-col-left-rail">
|
||||
<aside class="card-preview" data-card-name="{{ commander|urlencode }}">
|
||||
<a href="https://scryfall.com/search?q={{ commander|urlencode }}" target="_blank" rel="noopener">
|
||||
<img src="https://api.scryfall.com/cards/named?fuzzy={{ commander|urlencode }}&format=image&version=normal" alt="{{ commander }} card image" />
|
||||
</a>
|
||||
</aside>
|
||||
<div class="grow">
|
||||
<div hx-get="/build/banner?step=Ideal%20Counts&i=3&n=5" hx-trigger="load"></div>
|
||||
|
||||
|
||||
|
||||
{% if error %}
|
||||
<div style="color:#a00; margin:.5rem 0;">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
<form hx-post="/build/step3" hx-target="#wizard" hx-swap="innerHTML">
|
||||
<fieldset>
|
||||
<legend>Card Type Targets</legend>
|
||||
<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap:.75rem;">
|
||||
{% for key, label in labels.items() %}
|
||||
<label>
|
||||
{{ label }}
|
||||
<input type="number" name="{{ key }}" min="0" value="{{ (values or defaults)[key] }}" />
|
||||
<small class="muted">Default: {{ defaults[key] }}</small>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div style="margin-top:1rem; display:flex; gap:.5rem;">
|
||||
<button type="submit">Continue to Review</button>
|
||||
<button type="button" hx-get="/build/step2" hx-target="#wizard" hx-swap="innerHTML">Back</button>
|
||||
</div>
|
||||
</form>
|
||||
<div style="margin-top:.5rem;">
|
||||
<form action="/build" method="get" style="display:inline; margin:0;">
|
||||
<button type="submit">Start over</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
28
code/web/templates/build/_step4.html
Normal file
28
code/web/templates/build/_step4.html
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<section>
|
||||
<h3>Step 4: Review</h3>
|
||||
<div class="two-col two-col-left-rail">
|
||||
<aside class="card-preview" data-card-name="{{ commander|urlencode }}">
|
||||
<a href="https://scryfall.com/search?q={{ commander|urlencode }}" target="_blank" rel="noopener">
|
||||
<img src="https://api.scryfall.com/cards/named?fuzzy={{ commander|urlencode }}&format=image&version=normal" alt="{{ commander }} card image" />
|
||||
</a>
|
||||
</aside>
|
||||
<div class="grow">
|
||||
<div hx-get="/build/banner?step=Review&i=4&n=5" hx-trigger="load"></div>
|
||||
<h4>Chosen Ideals</h4>
|
||||
<ul>
|
||||
{% for key, label in labels.items() %}
|
||||
<li>{{ label }}: <strong>{{ values[key] }}</strong></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div style="margin-top:1rem; display:flex; gap:.5rem;">
|
||||
<form action="/build/step5/start" method="post" hx-post="/build/step5/start" hx-target="#wizard" hx-swap="innerHTML" style="display:inline; margin:0;">
|
||||
<button type="submit">Build Deck</button>
|
||||
</form>
|
||||
<button type="button" hx-get="/build/step3" hx-target="#wizard" hx-swap="innerHTML">Back</button>
|
||||
<form action="/build" method="get" style="display:inline; margin:0;">
|
||||
<button type="submit">Start over</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
111
code/web/templates/build/_step5.html
Normal file
111
code/web/templates/build/_step5.html
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
<section>
|
||||
<h3>Step 5: Build</h3>
|
||||
<div class="two-col two-col-left-rail">
|
||||
<aside class="card-preview">
|
||||
<a href="https://scryfall.com/search?q={{ commander|urlencode }}" target="_blank" rel="noopener">
|
||||
<img src="https://api.scryfall.com/cards/named?fuzzy={{ commander|urlencode }}&format=image&version=normal" alt="{{ commander }} card image" />
|
||||
</a>
|
||||
{% if status and status.startswith('Build complete') %}
|
||||
<div style="margin-top:.75rem; display:flex; gap:.35rem; flex-wrap:wrap;">
|
||||
{% if csv_path %}
|
||||
<form action="/files" method="get" target="_blank" style="display:inline; margin:0;">
|
||||
<input type="hidden" name="path" value="{{ csv_path }}" />
|
||||
<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 %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</aside>
|
||||
<div class="grow">
|
||||
<div hx-get="/build/banner?step=Build&i=5&n=5" hx-trigger="load"></div>
|
||||
|
||||
<p>Commander: <strong>{{ commander }}</strong></p>
|
||||
<p>Tags: {{ tags|default([])|join(', ') }}</p>
|
||||
<p>Bracket: {{ bracket }}</p>
|
||||
|
||||
{% if i and n %}
|
||||
<div class="muted" style="margin:.25rem 0 .5rem 0;">Stage {{ i }}/{{ n }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if status %}
|
||||
<div style="margin-top:1rem;">
|
||||
<strong>Status:</strong> {{ status }}{% if stage_label %} — <em>{{ stage_label }}</em>{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Controls moved back above the cards as requested -->
|
||||
<div style="margin-top:1rem; display:flex; gap:.5rem; flex-wrap:wrap; align-items:center;">
|
||||
<form hx-post="/build/step5/start" hx-target="#wizard" hx-swap="innerHTML" style="display:inline; margin-right:.5rem;">
|
||||
<button type="submit">Start Build</button>
|
||||
</form>
|
||||
<form hx-post="/build/step5/continue" hx-target="#wizard" hx-swap="innerHTML" style="display:inline;">
|
||||
<button type="submit" {% if status and status.startswith('Build complete') %}disabled{% endif %}>Continue</button>
|
||||
</form>
|
||||
<form hx-post="/build/step5/rerun" hx-target="#wizard" hx-swap="innerHTML" style="display:inline;">
|
||||
<button type="submit" {% if status and status.startswith('Build complete') %}disabled{% endif %}>Rerun Stage</button>
|
||||
</form>
|
||||
<button type="button" hx-get="/build/step4" hx-target="#wizard" hx-swap="innerHTML">Back</button>
|
||||
</div>
|
||||
|
||||
{% if added_cards %}
|
||||
<h4 style="margin-top:1rem;">Cards added this stage</h4>
|
||||
{% if stage_label and stage_label.startswith('Creatures') %}
|
||||
{% set groups = added_cards|groupby('sub_role') %}
|
||||
{% for g in groups %}
|
||||
{% set role = g.grouper %}
|
||||
{% if role %}
|
||||
{% set heading = 'Theme: ' + role.title() %}
|
||||
{% else %}
|
||||
{% set heading = 'Additional Picks' %}
|
||||
{% endif %}
|
||||
<h5 style="margin:.5rem 0 .25rem 0;">{{ heading }}</h5>
|
||||
<div class="card-grid">
|
||||
{% for c in g.list %}
|
||||
<div class="card-tile{% if game_changers and (c.name in game_changers) %} game-changer{% endif %}" data-card-name="{{ c.name }}" data-role="{{ c.role or c.sub_role or '' }}" data-tags="{{ (c.tags|join(', ')) if c.tags else '' }}">
|
||||
<a href="https://scryfall.com/search?q={{ c.name|urlencode }}" target="_blank" rel="noopener">
|
||||
<img src="https://api.scryfall.com/cards/named?fuzzy={{ c.name|urlencode }}&format=image&version=normal" alt="{{ c.name }} image" width="160" />
|
||||
</a>
|
||||
<div class="name">{{ c.name }}{% if c.count and c.count > 1 %} ×{{ c.count }}{% endif %}</div>
|
||||
{% if c.reason %}<div class="reason">{{ c.reason }}</div>{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="card-grid">
|
||||
{% for c in added_cards %}
|
||||
<div class="card-tile{% if game_changers and (c.name in game_changers) %} game-changer{% endif %}" data-card-name="{{ c.name }}" data-role="{{ c.role or c.sub_role or '' }}" data-tags="{{ (c.tags|join(', ')) if c.tags else '' }}">
|
||||
<a href="https://scryfall.com/search?q={{ c.name|urlencode }}" target="_blank" rel="noopener">
|
||||
<img src="https://api.scryfall.com/cards/named?fuzzy={{ c.name|urlencode }}&format=image&version=normal" alt="{{ c.name }} image" width="160" />
|
||||
</a>
|
||||
<div class="name">{{ c.name }}{% if c.count and c.count > 1 %} ×{{ c.count }}{% endif %}</div>
|
||||
{% if c.reason %}<div class="reason">{{ c.reason }}</div>{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if show_logs and log %}
|
||||
<details style="margin-top:1rem;">
|
||||
<summary>Show logs</summary>
|
||||
<pre style="margin-top:.5rem; white-space:pre-wrap; background:#0f1115; border:1px solid var(--border); padding:1rem; border-radius:8px; max-height:40vh; overflow:auto;">{{ log }}</pre>
|
||||
</details>
|
||||
{% endif %}
|
||||
|
||||
<!-- controls now above -->
|
||||
|
||||
{% if status and status.startswith('Build complete') %}
|
||||
{% if summary %}
|
||||
{% include "partials/deck_summary.html" %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
10
code/web/templates/build/index.html
Normal file
10
code/web/templates/build/index.html
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{% extends "base.html" %}
|
||||
{% block banner_subtitle %}Build a Deck{% endblock %}
|
||||
{% block content %}
|
||||
<h2>Build a Deck</h2>
|
||||
<div id="wizard">
|
||||
<div hx-get="/build/step1" hx-trigger="load" hx-target="#wizard" hx-swap="innerHTML"></div>
|
||||
<div hx-get="/build/banner?step=Build%20a%20Deck&i=1&n=5" hx-trigger="load"></div>
|
||||
<noscript><p>Enable JavaScript to use the wizard.</p></noscript>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Loading…
Add table
Add a link
Reference in a new issue