2025-10-06 09:17:59 -07:00
{% set prefix = partner_id_prefix if partner_id_prefix is defined else 'partner' %}
{% set feature_available = partner_feature_available if partner_feature_available is defined else False %}
{% set partner_capable = partner_capable if partner_capable is defined else False %}
{% set partner_options = partner_options if partner_options is defined else [] %}
{% set background_options = background_options if background_options is defined else [] %}
{% set partner_select_label = partner_select_label if partner_select_label is defined else 'Partner commander' %}
{% set partner_select_placeholder = partner_select_placeholder if partner_select_placeholder is defined else 'Select a partner' %}
{% set partner_auto_assigned = partner_auto_assigned if partner_auto_assigned is defined else False %}
{% set partner_auto_opt_out = partner_auto_opt_out if partner_auto_opt_out is defined else False %}
{% set partner_auto_default = partner_auto_default if partner_auto_default is defined else None %}
{% set partner_prefill_available = partner_prefill_available if partner_prefill_available is defined else False %}
{% set partner_note_id = prefix ~ '-partner-autonote' %}
{% set partner_warning_id = prefix ~ '-partner-warnings' %}
{% set partner_suggestions_enabled = partner_suggestions_enabled if partner_suggestions_enabled is defined else False %}
{% set partner_suggestions = partner_suggestions if partner_suggestions is defined else [] %}
{% set partner_suggestions_hidden = partner_suggestions_hidden if partner_suggestions_hidden is defined else [] %}
{% set partner_suggestions_total = partner_suggestions_total if partner_suggestions_total is defined else 0 %}
{% set partner_suggestions_metadata = partner_suggestions_metadata if partner_suggestions_metadata is defined else {} %}
{% set partner_suggestions_loaded = partner_suggestions_loaded if partner_suggestions_loaded is defined else False %}
{% set partner_suggestions_error = partner_suggestions_error if partner_suggestions_error is defined else None %}
{% set partner_suggestions_available = partner_suggestions_available if partner_suggestions_available is defined else False %}
{% set partner_suggestions_has_hidden = partner_suggestions_has_hidden if partner_suggestions_has_hidden is defined else False %}
{% if feature_available %}
< fieldset >
< legend > Partner Mechanics< / legend >
{% if not partner_capable %}
< p class = "muted" style = "font-size:12px;" > This commander doesn't support partner mechanics or backgrounds.< / p >
{% else %}
< input type = "hidden" name = "partner_enabled" value = "{{ partner_hidden_value or '1' }}" / >
< input type = "hidden" name = "partner_auto_opt_out" value = "{{ '1' if partner_auto_opt_out else '0' }}" data-partner-auto-opt = "{{ prefix }}" / >
< input type = "hidden" name = "partner_selection_source" value = "" data-partner-selection-source = "{{ prefix }}" / >
< div class = "muted" style = "font-size:12px; margin-bottom:.5rem;" > Choose either a partner commander or a background—never both.< / div >
{% if partner_role_hint %}
< div class = "muted" style = "font-size:12px; margin-bottom:.35rem;" > {{ partner_role_hint }}< / div >
{% endif %}
{% if primary_partner_with %}
< div class = "muted" style = "font-size:12px; margin-bottom:.35rem;" >
Pairs naturally with < strong > {{ primary_partner_with|join(', ') }}< / strong > .
< / div >
{% endif %}
{% if partner_options and partner_options|length and (not background_options or not background_options|length) %}
< div class = "muted" style = "font-size:12px; margin-bottom:.35rem;" > No Backgrounds available for this commander.< / div >
{% elif background_options and background_options|length and (not partner_options or not partner_options|length) %}
< div class = "muted" style = "font-size:12px; margin-bottom:.35rem;" > This commander can't select a partner commander but can choose a Background.< / div >
{% endif %}
{% if partner_error %}
< div style = "color:#a00; margin-bottom:.5rem; font-weight:600;" > {{ partner_error }}< / div >
{% endif %}
< div id = "{{ partner_note_id }}" class = "partner-autonote" data-partner-autonote = "{{ prefix }}" data-autonote = "{{ partner_auto_note or '' }}" style = "color:#046d1f; margin-bottom:.5rem; font-size:12px;" role = "status" aria-live = "polite" aria-atomic = "true" aria-hidden = "{{ 'false' if partner_auto_note else 'true' }}" { % if not partner_auto_note % } hidden { % endif % } >
< span class = "sr-only" > Partner pairing update:< / span >
< span data-partner-note-copy > {% if partner_auto_note %}{{ partner_auto_note }}{% endif %}< / span >
< / div >
{% if partner_prefill_available and partner_auto_default %}
< div style = "display:flex; align-items:center; gap:.5rem; margin-bottom:.5rem; flex-wrap:wrap;" >
< button type = "button" class = "chip{% if not partner_auto_opt_out %} active{% endif %}" data-partner-autotoggle = "{{ prefix }}" data-partner-default = "{{ partner_auto_default }}" aria-pressed = "{% if not partner_auto_opt_out %}true{% else %}false{% endif %}" aria-describedby = "{{ partner_note_id }}" >
{% if partner_auto_opt_out %}Enable default partner{% else %}Use default partner ({{ partner_auto_default }}){% endif %}
< / button >
< span class = "muted" style = "font-size:12px;" > Toggle to opt-out and choose a different partner.< / span >
< / div >
{% endif %}
{% if partner_suggestions_enabled %}
< div class = "partner-suggestions" data-partner-suggestions = "{{ prefix }}" data-partner-scope = "{{ partner_scope if partner_scope is defined else prefix }}" data-api-endpoint = "{{ partner_suggestions_endpoint if partner_suggestions_endpoint is defined else '/api/partner/suggestions' }}" data-primary-name = "{{ primary_commander_display }}" data-suggestions-json = '{{ partner_suggestions | tojson }}' data-hidden-json = '{{ partner_suggestions_hidden | tojson }}' data-total = "{{ partner_suggestions_total }}" data-loaded = "{{ '1' if partner_suggestions_loaded else '0' }}" data-error = "{{ partner_suggestions_error or '' }}" data-has-hidden = "{{ '1' if partner_suggestions_has_hidden else '0' }}" data-available = "{{ '1' if partner_suggestions_available else '0' }}" data-metadata-json = '{{ partner_suggestions_metadata | tojson }}' style = "display:grid; gap:.35rem; margin-bottom:.75rem;" >
< div class = "partner-suggestions__header" style = "display:flex; justify-content:space-between; align-items:center; gap:.5rem; flex-wrap:wrap;" >
< span style = "font-weight:600;" > Suggested partners< / span >
< div class = "partner-suggestions__controls" style = "display:flex; gap:.35rem; flex-wrap:wrap;" >
< button type = "button" class = "chip" data-partner-suggestions-refresh = "{{ prefix }}" aria-label = "Refresh partner suggestions" > Refresh< / button >
< button type = "button" class = "chip" data-partner-suggestions-more = "{{ prefix }}" hidden > Show more< / button >
< / div >
< / div >
< div class = "partner-suggestions__list" data-partner-suggestions-list style = "display:flex; flex-wrap:wrap; gap:.35rem;" > < / div >
< div class = "partner-suggestions__meta muted" data-partner-suggestions-meta style = "font-size:12px;" > < / div >
< / div >
{% endif %}
< div class = "partner-controls" data-partner-controls = "{{ prefix }}" data-partner-scope = "{{ partner_scope if partner_scope is defined else prefix }}" data-primary-name = "{{ primary_commander_display }}" style = "display:grid; gap:.5rem; margin-bottom:.5rem;" >
{% if partner_options and partner_options|length %}
< label style = "display:grid; gap:.35rem;" >
< span > {{ partner_select_label }}< / span >
< select name = "secondary_commander" id = "{{ prefix }}-partner-secondary" data-partner-select = "secondary" aria-describedby = "{{ partner_note_id }} {{ partner_warning_id }}" >
< option value = "" > {{ partner_select_placeholder }}< / option >
{% for opt in partner_options %}
{% set is_selected = (selected_secondary_commander|lower == opt.name|lower) %}
< option value = "{{ opt.name }}" data-pairing-mode = "{{ opt.pairing_mode }}" data-mode-label = "{{ opt.mode_label }}" data-color-label = "{{ opt.color_label }}" data-color-code = "{{ opt.color_code }}" data-image-url = "{{ opt.image_url or '' }}" data-scryfall-url = "{{ opt.scryfall_url or '' }}" data-role-label = "{{ opt.role_label or 'Partner commander' }}" { % if is_selected % } selected { % endif % } >
{{ opt.name }} — {{ opt.color_label }}
{% if opt.pairing_mode == 'partner_with' %}(Partner With){% elif opt.pairing_mode == 'partner_restricted' and opt.restriction_label %} (Partner - {{ opt.restriction_label }}){% elif opt.pairing_mode == 'doctor_companion' and opt.role_label %} ({{ opt.role_label }}){% endif %}
< / option >
{% endfor %}
< / select >
< / label >
{% endif %}
{% if background_options and background_options|length %}
< label style = "display:grid; gap:.35rem;" >
< span > Background< / span >
< select name = "background" id = "{{ prefix }}-partner-background" data-partner-select = "background" aria-describedby = "{{ partner_note_id }} {{ partner_warning_id }}" >
< option value = "" > Select a background< / option >
{% for opt in background_options %}
< option value = "{{ opt.name }}" data-color-label = "{{ opt.color_label }}" data-color-code = "{{ opt.color_code if opt.color_code is defined else '' }}" data-image-url = "{{ opt.image_url or '' }}" data-scryfall-url = "{{ opt.scryfall_url or '' }}" data-role-label = "{{ opt.role_label or 'Background' }}" { % if selected_background = = opt . name % } selected { % endif % } > {{ opt.name }} — {{ opt.color_label }}< / option >
{% endfor %}
< / select >
< / label >
{% endif %}
< / div >
< div style = "display:flex; gap:.5rem; margin-bottom:.5rem; flex-wrap:wrap;" >
< button type = "button" class = "chip" data-partner-clear = "{{ prefix }}" > Clear selection< / button >
< / div >
< div class = "partner-preview" data-partner-preview = "{{ prefix }}" { % if partner_preview % } data-preview-json = '{{ partner_preview | tojson }}' { % else % } hidden { % endif % } >
{% if partner_preview %}
{% set preview_image = partner_preview.secondary_image_url or partner_preview.image_url %}
{% if not preview_image and partner_preview.secondary_name %}
2025-10-28 08:21:52 -07:00
{% set preview_image = partner_preview.secondary_name|card_image('normal') %}
2025-10-06 09:17:59 -07:00
{% endif %}
{% set preview_href = partner_preview.secondary_scryfall_url or partner_preview.scryfall_url %}
{% if not preview_href and partner_preview.secondary_name %}
{% set preview_href = 'https://scryfall.com/search?q=' ~ partner_preview.secondary_name|urlencode %}
{% endif %}
{% set preview_role = partner_preview.secondary_role_label or partner_preview.role_label %}
{% set preview_primary = partner_preview.primary_name or primary_commander_display %}
{% set preview_secondary = partner_preview.secondary_name %}
{% set preview_themes = partner_preview.theme_tags %}
{% set preview_mode_label = partner_preview.partner_mode_label %}
{% set preview_color_label = partner_preview.color_label %}
< div class = "partner-preview__layout" >
{% if preview_image %}
< div class = "partner-preview__art" >
{% if preview_href %}< a href = "{{ preview_href }}" target = "_blank" rel = "noopener" > {% endif %}
< img src = "{{ preview_image }}" alt = "{{ (preview_secondary or 'Selected card') ~ ' card image' }}" loading = "lazy" decoding = "async" / >
{% if preview_href %}< / a > {% endif %}
< / div >
{% endif %}
< div class = "partner-preview__details" >
< div class = "partner-preview__header" > {{ preview_mode_label }}{% if preview_color_label %} • {{ preview_color_label }}{% endif %}< / div >
{% if preview_role %}
< div class = "partner-preview__role" > {{ preview_role }}< / div >
{% endif %}
{% if preview_secondary %}
< div class = "partner-preview__pairing" > Pairing: {{ preview_primary }}{% if preview_secondary %} + {{ preview_secondary }}{% endif %}< / div >
{% endif %}
{% if preview_themes %}
< div class = "partner-preview__themes muted" > Theme emphasis: {{ preview_themes|join(', ') }}< / div >
{% endif %}
< / div >
< / div >
{% endif %}
< / div >
< div id = "{{ partner_warning_id }}" data-partner-warnings = "{{ prefix }}" data-warnings-json = '{{ (partner_warnings or []) | tojson }}' style = "background:#fff7e5; border:1px solid #f0c36d; border-radius:8px; padding:.75rem; font-size:12px; color:#7a4b02;" role = "alert" aria-live = "polite" aria-hidden = "{{ 'false' if partner_warnings and partner_warnings|length else 'true' }}" { % if not ( partner_warnings and partner_warnings | length ) % } hidden { % endif % } >
{% if partner_warnings and partner_warnings|length %}
< strong > Warnings< / strong >
< ul style = "margin:.35rem 0 0 1.1rem;" >
{% for warn in partner_warnings %}
< li > {{ warn }}< / li >
{% endfor %}
< / ul >
{% endif %}
< / div >
{% endif %}
< / fieldset >
< script >
(function(){
var prefix = '{{ prefix }}';
var controls = document.querySelector('[data-partner-controls="' + prefix + '"]');
if (!controls || controls.dataset.partnerInit === '1') return;
controls.dataset.partnerInit = '1';
var scope = controls.getAttribute('data-partner-scope') || prefix;
var selects = Array.prototype.slice.call(controls.querySelectorAll('[data-partner-select]'));
var clearBtn = document.querySelector('[data-partner-clear="' + prefix + '"]');
var optInput = document.querySelector('input[name="partner_auto_opt_out"][data-partner-auto-opt="' + prefix + '"]');
var autoToggle = document.querySelector('[data-partner-autotoggle="' + prefix + '"]');
var defaultPartner = autoToggle ? autoToggle.getAttribute('data-partner-default') : null;
var previewBox = document.querySelector('[data-partner-preview="' + prefix + '"]');
var warningsBox = document.querySelector('[data-partner-warnings="' + prefix + '"]');
var autoNoteBox = document.querySelector('[data-partner-autonote="' + prefix + '"]');
var autoNoteCopy = autoNoteBox ? autoNoteBox.querySelector('[data-partner-note-copy]') : null;
var primaryName = controls.getAttribute('data-primary-name') || '';
var fieldset = controls.closest('fieldset');
var partnerEnabledInput = fieldset ? fieldset.querySelector('input[name="partner_enabled"]') : null;
var selectionSourceInput = fieldset ? fieldset.querySelector('input[name="partner_selection_source"][data-partner-selection-source="' + prefix + '"]') : null;
var initialAutoNote = autoNoteBox ? (autoNoteBox.getAttribute('data-autonote') || '') : '';
function setSelectionSource(value){
if (!selectionSourceInput) return;
if (value & & typeof value === 'string'){
selectionSourceInput.value = value;
} else {
selectionSourceInput.value = '';
}
}
function updateSuggestionsMeta(){
if (!suggestionsMeta || !suggestionsState){ return; }
var message = '';
var isError = false;
if (suggestionsState.loading){
message = 'Loading partner suggestions…';
} else if (suggestionsState.error){
message = suggestionsState.error;
isError = true;
} else if (suggestionsState.visible & & suggestionsState.visible.length){
var shown = suggestionsState.visible.length;
if (suggestionsState.expanded & & Array.isArray(suggestionsState.hidden)){
shown += suggestionsState.hidden.length;
}
if (suggestionsState.total & & suggestionsState.total > 0){
message = 'Showing ' + shown + ' of ' + suggestionsState.total + ' suggestions.';
} else {
message = 'Suggestions generated from recent deck data.';
}
var meta = suggestionsState.metadata || {};
if (meta.generated_at){
message += ' Updated ' + meta.generated_at + '.';
}
} else if (suggestionsState.loaded){
message = 'No partner suggestions available for this commander yet.';
} else {
message = '';
}
suggestionsMeta.textContent = message;
suggestionsMeta.hidden = !message;
if (isError){
suggestionsMeta.style.color = '#a00';
} else {
suggestionsMeta.style.color = '';
}
}
function markSuggestionActive(){
if (!suggestionsList || !suggestionsState){ return; }
var partnerSel = controls.querySelector('[data-partner-select="secondary"]');
var bgSel = controls.querySelector('[data-partner-select="background"]');
var partnerValue = partnerSel & & partnerSel.value ? partnerSel.value.toLowerCase() : '';
var backgroundValue = bgSel & & bgSel.value ? bgSel.value.toLowerCase() : '';
var buttons = suggestionsList.querySelectorAll('[data-partner-suggestion]');
buttons.forEach(function(btn){
var mode = (btn.getAttribute('data-mode') || 'partner').toLowerCase();
var name = (btn.getAttribute('data-name') || '').toLowerCase();
var active = false;
if (mode === 'background'){
active = !!backgroundValue & & backgroundValue === name;
} else {
active = !!partnerValue & & partnerValue === name;
}
btn.classList.toggle('active', active);
btn.setAttribute('aria-pressed', active ? 'true' : 'false');
});
}
function renderSuggestions(){
if (!suggestionsBox || !suggestionsList || !suggestionsState){
return;
}
suggestionsList.innerHTML = '';
if (suggestionsState.error){
updateSuggestionsMeta();
if (suggestionsMoreButton){ suggestionsMoreButton.hidden = true; }
return;
}
var items = Array.isArray(suggestionsState.visible) ? suggestionsState.visible.slice() : [];
if (suggestionsState.expanded & & Array.isArray(suggestionsState.hidden)){
items = items.concat(suggestionsState.hidden);
}
if (!items.length){
updateSuggestionsMeta();
if (suggestionsMoreButton){ suggestionsMoreButton.hidden = true; }
return;
}
items.forEach(function(item){
if (!item || !item.name) return;
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'chip partner-suggestion-chip';
btn.style.display = 'flex';
btn.style.flexDirection = 'column';
btn.style.alignItems = 'flex-start';
btn.style.gap = '2px';
btn.setAttribute('data-partner-suggestion', '1');
btn.setAttribute('data-mode', item.mode || 'partner');
btn.setAttribute('data-name', item.name);
if (item.mode_label){ btn.setAttribute('data-mode-label', item.mode_label); }
if (item.summary){ btn.setAttribute('data-summary', item.summary); }
if (typeof item.score_percent === 'number'){ btn.setAttribute('data-score', String(item.score_percent)); }
var titleParts = [];
if (item.summary){ titleParts.push(item.summary); }
if (Array.isArray(item.reasons) & & item.reasons.length){ titleParts = titleParts.concat(item.reasons); }
if (titleParts.length){ btn.title = titleParts.join(' • '); }
var nameSpan = document.createElement('span');
nameSpan.className = 'partner-suggestion-chip__name';
nameSpan.textContent = item.name;
nameSpan.style.fontWeight = '600';
btn.appendChild(nameSpan);
var summaryText = '';
if (item.summary){ summaryText = item.summary; }
else if (typeof item.score_percent === 'number'){ summaryText = item.score_percent + '% match'; }
else if (item.mode_label){ summaryText = item.mode_label; }
if (summaryText){
var summarySpan = document.createElement('span');
summarySpan.className = 'partner-suggestion-chip__meta muted';
summarySpan.textContent = summaryText;
summarySpan.style.fontSize = '11px';
summarySpan.style.opacity = '0.85';
btn.appendChild(summarySpan);
}
suggestionsList.appendChild(btn);
});
if (suggestionsMoreButton){
if (!suggestionsState.expanded & & Array.isArray(suggestionsState.hidden) & & suggestionsState.hidden.length){
suggestionsMoreButton.hidden = false;
suggestionsMoreButton.textContent = 'Show more (' + suggestionsState.hidden.length + ')';
} else {
suggestionsMoreButton.hidden = true;
}
}
markSuggestionActive();
updateSuggestionsMeta();
}
function revealHiddenSuggestions(){
if (!suggestionsState){ return; }
if (Array.isArray(suggestionsState.hidden) & & suggestionsState.hidden.length){
suggestionsState.expanded = true;
renderSuggestions();
} else {
fetchSuggestions({ includeHidden: true });
}
}
function collectSelectNames(kind){
var selector = '[data-partner-select="' + kind + '"]';
var sel = controls.querySelector(selector);
if (!sel){ return []; }
var values = [];
Array.prototype.forEach.call(sel.options, function(opt){
if (!opt || !opt.value){ return; }
values.push(opt.value);
});
return values;
}
function setSuggestionsLoading(flag){
if (!suggestionsState){ return; }
suggestionsState.loading = !!flag;
if (suggestionsRefreshButton){
if (flag){
suggestionsRefreshButton.classList.add('loading');
suggestionsRefreshButton.setAttribute('aria-busy', 'true');
} else {
suggestionsRefreshButton.classList.remove('loading');
suggestionsRefreshButton.removeAttribute('aria-busy');
}
}
updateSuggestionsMeta();
}
function fetchSuggestions(options){
if (!suggestionsBox || !suggestionsState){ return; }
if (typeof window === 'undefined' || typeof window.fetch !== 'function'){ return; }
if (!primaryName){ return; }
var includeHidden = !!(options & & options.includeHidden);
try {
var endpoint = suggestionsBox.getAttribute('data-api-endpoint') || '/api/partner/suggestions';
var params = new URLSearchParams();
params.set('commander', primaryName);
var partnerNames = collectSelectNames('secondary');
var backgroundNames = collectSelectNames('background');
partnerNames.forEach(function(name){ params.append('partner', name); });
backgroundNames.forEach(function(name){ params.append('background', name); });
params.set('limit', '8');
params.set('visible_limit', '3');
var modeSet = {};
var modes = (options & & Array.isArray(options.modes)) ? options.modes : ['partner_with', 'partner', 'doctor_companion', 'background'];
modes.forEach(function(mode){
var normalized = String(mode || '').trim();
if (!normalized){ return; }
var lower = normalized.toLowerCase();
if (modeSet[lower]){ return; }
modeSet[lower] = true;
params.append('mode', normalized);
});
if (includeHidden){ params.set('include_hidden', '1'); }
if (options & & options.forceRefresh){ params.set('refresh', '1'); }
var fetchUrl = endpoint + (endpoint.indexOf('?') === -1 ? '?' : '& ') + params.toString();
if (suggestionsAbort){
try { suggestionsAbort.abort(); } catch(_){ }
}
suggestionsAbort = new AbortController();
setSuggestionsLoading(true);
fetch(fetchUrl, {
method: 'GET',
credentials: 'same-origin',
headers: { 'Accept': 'application/json' },
signal: suggestionsAbort.signal,
}).then(function(resp){
if (!resp.ok){
throw new Error('suggestions request failed');
}
return resp.json();
}).then(function(data){
suggestionsAbort = null;
suggestionsState.error = '';
suggestionsState.loaded = true;
suggestionsState.metadata = data & & data.metadata ? data.metadata : {};
suggestionsState.total = (data & & typeof data.total === 'number') ? data.total : 0;
suggestionsState.visible = Array.isArray(data & & data.visible) ? data.visible : [];
suggestionsState.hidden = Array.isArray(data & & data.hidden) ? data.hidden : [];
suggestionsState.expanded = includeHidden & & suggestionsState.hidden.length ? true : false;
suggestionsBox.setAttribute('data-loaded', '1');
suggestionsBox.setAttribute('data-error', '');
renderSuggestions();
}).catch(function(err){
if (err & & err.name === 'AbortError'){ return; }
suggestionsState.error = 'Unable to load partner suggestions.';
suggestionsBox.setAttribute('data-error', suggestionsState.error);
renderSuggestions();
}).finally(function(){
setSuggestionsLoading(false);
});
} catch(_err){
suggestionsState.error = 'Unable to load partner suggestions.';
renderSuggestions();
}
}
var initialWarnings = [];
if (warningsBox & & warningsBox.dataset.warningsJson){
try { initialWarnings = JSON.parse(warningsBox.dataset.warningsJson); }
catch(_){ initialWarnings = []; }
}
var serverPayload = null;
if (previewBox & & previewBox.dataset.previewJson){
try{ serverPayload = JSON.parse(previewBox.dataset.previewJson); }
catch(_){ serverPayload = null; }
}
setServerPayload(serverPayload);
var suggestionsBox = document.querySelector('[data-partner-suggestions="' + prefix + '"]');
var suggestionsList = null;
var suggestionsMeta = null;
var suggestionsMoreButton = null;
var suggestionsRefreshButton = null;
var suggestionsAbort = null;
var suggestionsState = null;
function parseSuggestionsAttr(element, attr, fallback){
if (!element){ return fallback; }
var raw = element.getAttribute(attr);
if (!raw){ return fallback; }
try { return JSON.parse(raw); }
catch(_){ return fallback; }
}
if (suggestionsBox){
suggestionsList = suggestionsBox.querySelector('[data-partner-suggestions-list]');
suggestionsMeta = suggestionsBox.querySelector('[data-partner-suggestions-meta]');
suggestionsMoreButton = suggestionsBox.querySelector('[data-partner-suggestions-more="' + prefix + '"]');
suggestionsRefreshButton = suggestionsBox.querySelector('[data-partner-suggestions-refresh="' + prefix + '"]');
suggestionsState = {
visible: parseSuggestionsAttr(suggestionsBox, 'data-suggestions-json', []),
hidden: parseSuggestionsAttr(suggestionsBox, 'data-hidden-json', []),
metadata: parseSuggestionsAttr(suggestionsBox, 'data-metadata-json', {}),
total: parseInt(suggestionsBox.getAttribute('data-total') || '0', 10) || 0,
error: suggestionsBox.getAttribute('data-error') || '',
loaded: suggestionsBox.getAttribute('data-loaded') === '1',
expanded: false,
loading: false,
};
}
var modeLabels = {
'partner': 'Partner',
'partner_with': 'Partner With',
'doctor_companion': "Doctor & Companion",
'background': 'Choose a Background'
};
function buildCardImageUrl(name){
if (!name) return '';
2025-10-28 08:21:52 -07:00
return '/api/images/normal/' + encodeURIComponent(name);
2025-10-06 09:17:59 -07:00
}
function buildScryfallUrl(name){
if (!name) return '';
return 'https://scryfall.com/search?q=' + encodeURIComponent(name);
}
function defaultRoleForMode(mode){
if (!mode) return '';
switch(String(mode).toLowerCase()){
case 'background':
return 'Background';
case 'doctor_companion':
return "Doctor pairing";
default:
return 'Partner commander';
}
}
var previewAbort = null;
if (typeof window !== 'undefined' & & !window.partnerPreviewState){
try { window.partnerPreviewState = {}; } catch(_){ }
}
function setPreviewState(detail){
if (typeof window === 'undefined') return;
if (!detail || typeof detail !== 'object') return;
var scopeKey = detail.scope || scope || prefix;
if (!scopeKey) return;
var store = window.partnerPreviewState || {};
store[scopeKey] = {
theme_tags: Array.isArray(detail.theme_tags) ? detail.theme_tags.slice() : [],
payload: detail.payload || null,
warnings: Array.isArray(detail.warnings) ? detail.warnings.slice() : [],
auto_note: detail.auto_note || null,
partner_mode: detail.partner_mode || null,
resolved_secondary: detail.resolved_secondary || null,
resolved_background: detail.resolved_background || null,
secondary_role_label: detail.secondary_role_label || detail.role_label || null,
};
try { window.partnerPreviewState = store; } catch(_){ }
}
function escapeHtml(str){
return String(str || "").replace(/[& < >"']/g, function(ch){
return ({"&":"& ","< ":"< ",">":"> ","\"":"" ","'":"' "}[ch]);
});
}
function clearPreview(){
if (!previewBox) return;
previewBox.hidden = true;
previewBox.innerHTML = '';
markSuggestionActive();
}
function renderPreview(payload){
if (!previewBox) return;
if (!payload){
clearPreview();
return;
}
var mode = payload.partner_mode || payload.mode || '';
var modeLabel = payload.partner_mode_label || payload.mode_label || modeLabels[mode] || 'Partner Mechanics';
var colorLabel = payload.color_label || '';
var secondaryName = payload.secondary_name || payload.name || '';
var primary = payload.primary_name || primaryName;
2025-10-28 08:21:52 -07:00
// Ensure theme_tags is always an array, even if it comes as a string or other type
var themes = Array.isArray(payload.theme_tags) ? payload.theme_tags :
(typeof payload.theme_tags === 'string' ? payload.theme_tags.split(',').map(function(t){ return t.trim(); }).filter(Boolean) : []);
2025-10-06 09:17:59 -07:00
var imageUrl = payload.secondary_image_url || payload.image_url || '';
if (!imageUrl & & secondaryName){
imageUrl = buildCardImageUrl(secondaryName);
}
var scryfallUrl = payload.secondary_scryfall_url || payload.scryfall_url || '';
if (!scryfallUrl & & secondaryName){
scryfallUrl = buildScryfallUrl(secondaryName);
}
var roleLabel = payload.secondary_role_label || payload.role_label || defaultRoleForMode(mode);
var html = '< div class = "partner-preview__layout" > ';
var normalizedTags = Array.isArray(themes) ? themes.filter(function(tag){ return tag & & String(tag).trim(); }).map(function(tag){ return String(tag).trim(); }) : [];
themes = normalizedTags;
var tagString = normalizedTags.length ? normalizedTags.join(', ') : '';
if (imageUrl){
var attrParts = [];
if (secondaryName){
attrParts.push('data-card-name="' + escapeHtml(secondaryName) + '"');
attrParts.push('data-original-name="' + escapeHtml(secondaryName) + '"');
}
if (roleLabel){
attrParts.push('data-role="' + escapeHtml(roleLabel) + '"');
}
if (tagString){
attrParts.push('data-tags="' + escapeHtml(tagString) + '"');
attrParts.push('data-overlaps="' + escapeHtml(tagString) + '"');
}
html += '< div class = "partner-preview__art card-preview" ' + ( attrParts . length ? ' ' + attrParts . join ( ' ' ) : ' ' ) + ' > ';
if (scryfallUrl){
html += '< a href = "' + escapeHtml(scryfallUrl) + '" target = "_blank" rel = "noopener" > ';
}
html += '< img src = "' + escapeHtml(imageUrl) + '" alt = "' + escapeHtml((secondaryName || 'Selected card') + ' card image') + '" loading = "lazy" decoding = "async" data-card-name = "' + escapeHtml(secondaryName || '') + '" ' ;
if (roleLabel){ html += ' data-role="' + escapeHtml(roleLabel) + '"'; }
if (tagString){ html += ' data-tags="' + escapeHtml(tagString) + '" data-overlaps="' + escapeHtml(tagString) + '"'; }
html += ' />';
if (scryfallUrl){
html += '< / a > ';
}
html += '< / div > ';
}
html += '< div class = "partner-preview__details" > ';
html += '< div class = "partner-preview__header" > ' + escapeHtml(modeLabel);
if (colorLabel){ html += ' • ' + escapeHtml(colorLabel); }
html += '< / div > ';
if (roleLabel){
html += '< div class = "partner-preview__role" > ' + escapeHtml(roleLabel) + '< / div > ';
}
if (secondaryName){
var pairing = escapeHtml(primary);
if (pairing){ pairing += ' + '; }
html += '< div class = "partner-preview__pairing" > Pairing: ' + pairing + escapeHtml(secondaryName) + '< / div > ';
}
if (themes & & themes.length){
html += '< div class = "partner-preview__themes muted" > Theme emphasis: ' + themes.map(escapeHtml).join(', ') + '< / div > ';
}
html += '< / div > < / div > ';
previewBox.innerHTML = html;
previewBox.hidden = false;
markSuggestionActive();
}
function setServerPayload(payload){
serverPayload = (payload & & typeof payload === 'object') ? payload : null;
if (!previewBox) return;
if (serverPayload){
try {
previewBox.setAttribute('data-preview-json', JSON.stringify(serverPayload));
} catch(_){
previewBox.removeAttribute('data-preview-json');
}
} else {
previewBox.removeAttribute('data-preview-json');
}
}
function updateAutoNote(note){
if (!autoNoteBox) return;
var text = (note & & String(note).trim()) || '';
autoNoteBox.setAttribute('aria-live', 'polite');
if (autoNoteCopy){
autoNoteCopy.textContent = text;
} else {
autoNoteBox.textContent = text;
}
autoNoteBox.hidden = !text;
autoNoteBox.setAttribute('aria-hidden', (!text).toString());
try { autoNoteBox.setAttribute('data-autonote', text); } catch(_){ }
}
function updateWarnings(list){
if (!warningsBox) return;
var warnings = Array.isArray(list) ? list.filter(function(msg){ return msg & & String(msg).trim(); }) : [];
try { warningsBox.setAttribute('data-warnings-json', JSON.stringify(warnings)); } catch(_){ }
if (!warnings.length){
warningsBox.innerHTML = '';
warningsBox.hidden = true;
warningsBox.setAttribute('aria-hidden', 'true');
return;
}
var html = '< strong > Warnings< / strong > < ul style = "margin:.35rem 0 0 1.1rem;" > ';
warnings.forEach(function(msg){
html += '< li > ' + escapeHtml(String(msg)) + '< / li > ';
});
html += '< / ul > ';
warningsBox.innerHTML = html;
warningsBox.hidden = false;
warningsBox.setAttribute('aria-hidden', 'false');
}
function dispatchPreview(detail){
if (typeof document === 'undefined') return;
setPreviewState(detail);
try {
document.dispatchEvent(new CustomEvent('partner:preview', { detail: detail }));
} catch(_){ }
}
function requestPreviewUpdate(){
if (typeof window === 'undefined' || typeof window.fetch !== 'function') return;
if (!primaryName) return;
var partnerSel = controls.querySelector('[data-partner-select="secondary"]');
var bgSel = controls.querySelector('[data-partner-select="background"]');
var secondaryVal = partnerSel ? (partnerSel.value || '') : '';
var bgVal = bgSel ? (bgSel.value || '') : '';
var enabledVal = partnerEnabledInput ? (partnerEnabledInput.value || '') : '1';
if (previewAbort){
try { previewAbort.abort(); } catch(_){ }
}
previewAbort = new AbortController();
var formData = new FormData();
formData.append('commander', primaryName);
formData.append('partner_enabled', enabledVal || '1');
formData.append('secondary_commander', secondaryVal);
formData.append('background', bgVal);
formData.append('partner_auto_opt_out', optInput ? (optInput.value || '0') : '0');
formData.append('scope', scope || prefix);
formData.append('selection_source', selectionSourceInput ? (selectionSourceInput.value || '') : '');
fetch('/build/partner/preview', {
method: 'POST',
body: formData,
credentials: 'same-origin',
headers: { 'Accept': 'application/json' },
signal: previewAbort.signal,
}).then(function(resp){
if (!resp.ok){ throw new Error('preview request failed'); }
return resp.json();
}).then(function(data){
previewAbort = null;
if (!data) return;
if (Object.prototype.hasOwnProperty.call(data, 'preview')){
setServerPayload(data.preview);
if (data.preview){ renderPreview(data.preview); }
else { clearPreview(); }
}
updateAutoNote(data & & data.auto_note);
updateWarnings(data & & data.warnings);
var evtDetail = {
scope: (data & & data.scope) || scope || prefix,
payload: (data & & data.preview) || null,
theme_tags: (data & & data.theme_tags) || [],
warnings: (data & & data.warnings) || [],
auto_note: (data & & data.auto_note) || null,
partner_mode: (data & & data.partner_mode) || null,
resolved_secondary: Object.prototype.hasOwnProperty.call(data || {}, 'resolved_secondary') ? (data & & data.resolved_secondary) : undefined,
resolved_background: Object.prototype.hasOwnProperty.call(data || {}, 'resolved_background') ? (data & & data.resolved_background) : undefined,
secondary_role_label: data & & data.preview ? (data.preview.secondary_role_label || data.preview.role_label || null) : null,
};
dispatchPreview(evtDetail);
if (partnerSel & & Object.prototype.hasOwnProperty.call(data, 'resolved_secondary')){
partnerSel.value = data.resolved_secondary || '';
}
if (bgSel & & Object.prototype.hasOwnProperty.call(data, 'resolved_background')){
bgSel.value = data.resolved_background || '';
}
}).catch(function(err){
if (err & & err.name === 'AbortError'){ return; }
previewAbort = null;
});
}
updateAutoNote(initialAutoNote);
updateWarnings(initialWarnings);
var initialHasPreview = !!(serverPayload & & Array.isArray(serverPayload.theme_tags) & & serverPayload.theme_tags.length);
if (initialHasPreview || initialWarnings.length || (initialAutoNote & & initialAutoNote.trim())){
setTimeout(function(){
dispatchPreview({
scope: scope || prefix,
payload: serverPayload,
theme_tags: (serverPayload & & serverPayload.theme_tags) || [],
warnings: initialWarnings,
auto_note: initialAutoNote || null,
partner_mode: serverPayload ? (serverPayload.partner_mode || serverPayload.mode || null) : null,
secondary_role_label: serverPayload ? (serverPayload.secondary_role_label || serverPayload.role_label || null) : null,
});
}, 0);
}
function renderFromServer(){
if (serverPayload){
renderPreview(serverPayload);
} else {
clearPreview();
}
}
function renderFromSelection(sel, modeOverride){
if (!sel){
if (serverPayload){ renderFromServer(); } else { clearPreview(); }
return;
}
var option = sel.options[sel.selectedIndex];
if (!option || !option.value){
if (serverPayload){ renderFromServer(); } else { clearPreview(); }
return;
}
var mode = modeOverride || option.getAttribute('data-pairing-mode') || 'partner';
var image = option.getAttribute('data-image-url') || '';
var link = option.getAttribute('data-scryfall-url') || '';
var role = option.getAttribute('data-role-label') || '';
if (!image){ image = buildCardImageUrl(option.value); }
if (!link){ link = buildScryfallUrl(option.value); }
if (!role){ role = defaultRoleForMode(mode); }
var payload = {
partner_mode: mode,
partner_mode_label: option.getAttribute('data-mode-label') || modeLabels[mode] || 'Partner Mechanics',
color_label: option.getAttribute('data-color-label') || '',
secondary_name: option.value,
primary_name: primaryName,
secondary_image_url: image,
secondary_scryfall_url: link,
secondary_role_label: role,
};
payload.secondary_role_label = role;
payload.theme_tags = payload.theme_tags || [];
renderPreview(payload);
}
function setOptOut(flag){
if (optInput){ optInput.value = flag ? '1' : '0'; }
if (autoToggle){
autoToggle.classList.toggle('active', !flag);
autoToggle.setAttribute('aria-pressed', (!flag).toString());
var label = flag ? 'Enable default partner' : 'Use default partner';
if (!flag & & defaultPartner){ label += ' (' + defaultPartner + ')'; }
autoToggle.textContent = label;
}
markSuggestionActive();
}
function applySuggestionSelection(mode, name){
if (!name){ return; }
setSelectionSource('suggestion');
var normalizedMode = String(mode || '').toLowerCase();
var partnerSel = controls.querySelector('[data-partner-select="secondary"]');
var bgSel = controls.querySelector('[data-partner-select="background"]');
if (normalizedMode === 'background'){
if (bgSel){ bgSel.value = name; }
if (partnerSel){ partnerSel.value = ''; }
if (autoToggle){ setOptOut(true); }
if (bgSel){
renderFromSelection(bgSel, 'background');
requestPreviewUpdate();
} else {
renderFromServer();
}
} else {
if (partnerSel){ partnerSel.value = name; }
if (bgSel){ bgSel.value = ''; }
if (autoToggle){
syncBySelection();
} else if (partnerSel){
renderFromSelection(partnerSel);
requestPreviewUpdate();
} else {
renderFromServer();
}
}
markSuggestionActive();
}
function syncBySelection(){
var partnerSel = controls.querySelector('[data-partner-select="secondary"]');
if (!partnerSel || !autoToggle || !defaultPartner) return;
if (partnerSel.value & & partnerSel.value.toLowerCase() === defaultPartner.toLowerCase()){
setOptOut(false);
renderFromSelection(partnerSel);
requestPreviewUpdate();
} else if (partnerSel.value) {
setOptOut(true);
renderFromSelection(partnerSel);
requestPreviewUpdate();
}
}
if (autoToggle){
autoToggle.addEventListener('click', function(){
var currentOptOut = optInput & & optInput.value === '1';
if (currentOptOut){
setOptOut(false);
setSelectionSource('auto');
if (defaultPartner){
var partnerSel = controls.querySelector('[data-partner-select="secondary"]');
if (partnerSel){ partnerSel.value = defaultPartner; }
var bgSel = controls.querySelector('[data-partner-select="background"]');
if (bgSel){ bgSel.value = ''; }
renderFromSelection(partnerSel);
} else {
renderFromServer();
}
requestPreviewUpdate();
} else {
setOptOut(true);
setSelectionSource('');
selects.forEach(function(sel){
if (sel & & sel.getAttribute('data-partner-select') === 'secondary'){
sel.value = '';
}
});
renderFromServer();
requestPreviewUpdate();
}
});
}
selects.forEach(function(sel){
sel.addEventListener('change', function(){
setSelectionSource('manual');
var key = sel.getAttribute('data-partner-select');
if (key === 'secondary' & & sel.value){
var bgSel = controls.querySelector('[data-partner-select="background"]');
if (bgSel){ bgSel.value = ''; }
if (autoToggle){
syncBySelection();
} else {
renderFromSelection(sel);
requestPreviewUpdate();
}
markSuggestionActive();
return;
}
if (key === 'background' & & sel.value){
var partnerSel = controls.querySelector('[data-partner-select="secondary"]');
if (partnerSel){ partnerSel.value = ''; }
if (autoToggle){ setOptOut(true); }
renderFromSelection(sel, 'background');
requestPreviewUpdate();
markSuggestionActive();
return;
}
if (!sel.value){
renderFromServer();
requestPreviewUpdate();
}
markSuggestionActive();
});
});
if (suggestionsState){
if (suggestionsList){
suggestionsList.addEventListener('click', function(evt){
var target = evt.target.closest('[data-partner-suggestion]');
if (!target){ return; }
evt.preventDefault();
var mode = target.getAttribute('data-mode') || 'partner';
var name = target.getAttribute('data-name') || '';
applySuggestionSelection(mode, name);
});
}
if (suggestionsMoreButton){
suggestionsMoreButton.addEventListener('click', function(){
revealHiddenSuggestions();
});
}
if (suggestionsRefreshButton){
suggestionsRefreshButton.addEventListener('click', function(){
fetchSuggestions({ includeHidden: suggestionsState.expanded, forceRefresh: true });
});
}
if (suggestionsState.visible & & suggestionsState.visible.length){
renderSuggestions();
} else if (suggestionsState.error){
updateSuggestionsMeta();
} else {
fetchSuggestions();
}
}
if (clearBtn){
clearBtn.addEventListener('click', function(){
selects.forEach(function(sel){ if (sel){ sel.value = ''; } });
if (autoToggle){ setOptOut(true); }
setSelectionSource('');
renderFromServer();
requestPreviewUpdate();
markSuggestionActive();
});
}
if (typeof window !== 'undefined' & & window.newDeckPartnerState){
try {
var restore = window.newDeckPartnerState;
var partnerSel = controls.querySelector('[data-partner-select="secondary"]');
var bgSel = controls.querySelector('[data-partner-select="background"]');
if (partnerSel & & restore.secondary){ partnerSel.value = restore.secondary; }
if (bgSel & & restore.background){ bgSel.value = restore.background; }
if (restore.enabled === false){ selects.forEach(function(sel){ if (sel){ sel.value = ''; } }); }
if (partnerSel & & partnerSel.value){ renderFromSelection(partnerSel); }
else if (bgSel & & bgSel.value){ renderFromSelection(bgSel, 'background'); }
delete window.newDeckPartnerState;
} catch(_){ }
}
if (optInput & & optInput.value === '1'){
setOptOut(true);
renderFromServer();
} else {
setOptOut(!defaultPartner);
if (defaultPartner){ syncBySelection(); }
else if (serverPayload){ renderFromServer(); }
}
markSuggestionActive();
try {
var slot = document.getElementById('newdeck-tags-slot');
if (slot) slot.setAttribute('data-has-content', '1');
} catch(_){ }
})();
< / script >
{% endif %}