PageSourceSearch

https://magicblogtk.web.app/js/components.js?v=18

js magicblogtk.web.app collected 2026-10-03 12:29:13 UTC 17,400 bytes, 378 lines download raw bytes

1import { getCreatorLinks } from './data.js?v=18';
2import { getExpansionEmoji, getManaCSSColors } from './utils.js?v=18';
3
4const MANA_COLORS = getManaCSSColors();
5
6function escapeHtml(value = '') {
7  return String(value)
8    .replaceAll('&', '&')
9    .replaceAll('<', '&lt;')
10    .replaceAll('>', '&gt;')
11    .replaceAll('"', '&quot;')
12    .replaceAll("'", '&#039;');
13}
14
15function icon(name) {
16  const icons = {
17    youtube: '<path d="M23.5 6.2a3 3 0 0 0-2.1-2.1C19.5 3.5 12 3.5 12 3.5s-7.5 0-9.4.6A3 3 0 0 0 .5 6.2 32 32 0 0 0 0 12a32 32 0 0 0 .5 5.8 3 3 0 0 0 2.1 2.1c1.9.6 9.4.6 9.4.6s7.5 0 9.4-.6a3 3 0 0 0 2.1-2.1A32 32 0 0 0 24 12a32 32 0 0 0-.5-5.8ZM9.5 15.6V8.4l6.3 3.6-6.3 3.6Z"/>',
18    external: '<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><path d="M15 3h6v6"/><path d="m10 14 11-11"/>',
19    search: '<circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/>',
20    refresh: '<path d="M21 12a9 9 0 0 1-15.2 6.5"/><path d="M3 12A9 9 0 0 1 18.2 5.5"/><path d="M18 2v4h-4"/><path d="M6 22v-4h4"/>',
21    x: '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
22    web: '<circle cx="12" cy="12" r="9"/><path d="M3 12h18"/><path d="M12 3c2.2 2.5 3.3 5.5 3.3 9S14.2 18.5 12 21c-2.2-2.5-3.3-5.5-3.3-9S9.8 5.5 12 3Z"/>',
23    twitch: '<path d="M4 4h16v10l-4 4h-4l-3 3v-3H4V4Z"/><path d="M10 8v5"/><path d="M15 8v5"/>',
24    kick: '<path d="M5 4h5v6l5-6h5l-6 7 6 9h-6l-4-6v6H5V4Z"/>',
25    untapped: '<path d="M5 19V5"/><path d="M10 19V9"/><path d="M15 19v-7"/><path d="M20 19V7"/>',
26    heart: '<path d="M20.8 5.6a5.1 5.1 0 0 0-7.2 0L12 7.2l-1.6-1.6a5.1 5.1 0 0 0-7.2 7.2L12 21.5l8.8-8.7a5.1 5.1 0 0 0 0-7.2Z"/>',
27  };
28
29  const fill = name === 'youtube' ? 'fill="currentColor"' : 'fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"';
30  return `<svg class="icon" viewBox="0 0 24 24" aria-hidden="true" ${fill}>${icons[name] || icons.external}</svg>`;
31}
32
33function renderSocialGrid(links) {
34  return ['youtube', 'twitch', 'kick', 'web', 'untapped', 'donate']
35    .filter(key => links[key]?.url)
36    .map(key => `
37      <a class="social-tile social-tile--${key}" href="${escapeHtml(links[key].url)}" target="_blank" rel="noopener" title="${escapeHtml(links[key].label)}">
38        ${icon(links[key].icon)}
39        <span>${escapeHtml(links[key].label)}</span>
40      </a>
41    `).join('');
42}
43
44function renderManaDots(colors = []) {
45  return colors.map(color => {
46    const label = { W: 'White', U: 'Blue', B: 'Black', R: 'Red', G: 'Green', C: 'Colorless' }[color] || color;
47    return `<span class="mana-dot" style="--mana:${MANA_COLORS[color] || MANA_COLORS.C}" title="${label}" aria-label="${label}"></span>`;
48  }).join('');
49}
50
51function renderFormatBadge(format) {
52  const cls = format.toLowerCase().replace(/\s+/g, '-');
53  return `<span class="badge badge--${cls}">${escapeHtml(format)}</span>`;
54}
55
56export function renderDeckCard(deck, index) {
57  const delay = Math.min(index * 24, 420);
58  const expansion = deck.expansion || deck.sourceSheet || 'Sin seccion';
59  const deckAction = deck.hasDeck
60    ? `<a class="deck-card__btn deck-card__btn--primary" href="${escapeHtml(deck.untappedUrl)}" target="_blank" rel="noopener">${icon('external')} Ver mazo</a>`
61    : `<span class="deck-card__btn deck-card__btn--disabled" aria-disabled="true">${icon('external')} Sin lista</span>`;
62  const videoAction = deck.hasVideo
63    ? `<a class="deck-card__btn deck-card__btn--video" href="${escapeHtml(deck.youtubeUrl)}" target="_blank" rel="noopener">${icon('youtube')} Video</a>`
64    : `<span class="deck-card__btn deck-card__btn--disabled" aria-disabled="true">${icon('youtube')} Sin video</span>`;
65
66  return `
67    <article class="deck-card" style="animation-delay:${delay}ms" id="deck-${escapeHtml(deck.id)}">
68      <div class="deck-card__top">
69        <div class="deck-card__colors">${renderManaDots(deck.colors)}</div>
70        <div class="deck-card__top-actions">
71          <button class="deck-card__detail" type="button" data-deck-detail="${escapeHtml(deck.id)}">Detalle</button>
72          ${renderFormatBadge(deck.format)}
73        </div>
74      </div>
75      <h3 class="deck-card__name">${escapeHtml(deck.name)}</h3>
76      <div class="deck-card__meta">
77        <span>${escapeHtml(deck.dateFormatted || 'Sin fecha')}</span>
78        <span>${escapeHtml(getExpansionEmoji(expansion))} ${escapeHtml(expansion)}</span>
79      </div>
80      <div class="deck-card__source">
81        <span>${deck.hasDeck ? 'Lista Untapped vinculada' : 'Lista pendiente'}</span>
82        <span>${deck.hasVideo ? 'Video directo' : 'Video pendiente'}</span>
83      </div>
84      <div class="deck-card__actions">
85        ${deckAction}
86        ${videoAction}
87      </div>
88    </article>`;
89}
90
91export function renderDeckDetailModal(deck) {
92  const expansion = deck.expansion || deck.sourceSheet || 'Sin seccion';
93  const deckAction = deck.hasDeck
94    ? `<a class="deck-card__btn deck-card__btn--primary" href="${escapeHtml(deck.untappedUrl)}" target="_blank" rel="noopener">${icon('external')} Abrir Untapped</a>`
95    : `<span class="deck-card__btn deck-card__btn--disabled" aria-disabled="true">${icon('external')} Sin lista Untapped</span>`;
96  const videoAction = deck.hasVideo
97    ? `<a class="deck-card__btn deck-card__btn--video" href="${escapeHtml(deck.youtubeUrl)}" target="_blank" rel="noopener">${icon('youtube')} Ver video</a>`
98    : `<span class="deck-card__btn deck-card__btn--disabled" aria-disabled="true">${icon('youtube')} Sin video</span>`;
99
100  return `
101    <div class="modal-backdrop" data-modal-close>
102      <section class="deck-modal" role="dialog" aria-modal="true" aria-labelledby="deck-modal-title">
103        <button class="modal-close" type="button" data-modal-close aria-label="Cerrar">${icon('x')}</button>
104        <div class="deck-modal__top">
105          <div class="deck-card__colors">${renderManaDots(deck.colors)}</div>
106          ${renderFormatBadge(deck.format)}
107        </div>
108        <h2 id="deck-modal-title">${escapeHtml(deck.name)}</h2>
109        <dl class="deck-modal__meta">
110          <div><dt>Fecha</dt><dd>${escapeHtml(deck.dateFormatted || 'Sin fecha')}</dd></div>
111          <div><dt>Seccion</dt><dd>${escapeHtml(expansion)}</dd></div>
112          <div><dt>Formato</dt><dd>${escapeHtml(deck.format)}</dd></div>
113          <div><dt>Estado</dt><dd>${deck.hasVideo && deck.hasDeck ? 'Video + mazo' : deck.hasVideo ? 'Solo video' : deck.hasDeck ? 'Solo lista' : 'Pendiente'}</dd></div>
114        </dl>
115        <div class="deck-modal__actions">
116          ${deckAction}
117          ${videoAction}
118        </div>
119      </section>
120    </div>`;
121}
122
123export function renderExpansionHeader(expansion, count) {
124  return `
125    <div class="expansion-header" id="exp-${escapeHtml(expansion.replace(/\s+/g, '-').toLowerCase())}">
126      <div>
127        <span class="expansion-header__kicker">${escapeHtml(getExpansionEmoji(expansion))} Seccion</span>
128        <h2>${escapeHtml(expansion)}</h2>
129      </div>
130      <span class="expansion-count">${count} mazos</span>
131    </div>`;
132}
133
134export function renderStatsBar(stats) {
135  return `
136    <section class="stats-bar" aria-label="Resumen de la coleccion">
137      <div class="stat-item"><strong>${stats.total}</strong><span>Mazos</span></div>
138      <div class="stat-item"><strong>${stats.withDeck}</strong><span>con Untapped</span></div>
139      <div class="stat-item"><strong>${stats.withVideo}</strong><span>con video</span></div>
140      <div class="stat-item"><strong>${escapeHtml(stats.latestDate)}</strong><span>Ultimo mazo</span></div>
141    </section>`;
142}
143
144export function renderFilterBar(expansions, formats) {
145  const manaButtons = [
146    { code: 'W', label: 'White' },
147    { code: 'U', label: 'Blue' },
148    { code: 'B', label: 'Black' },
149    { code: 'R', label: 'Red' },
150    { code: 'G', label: 'Green' },
151  ];
152
153  return `
154    <section class="filter-bar" id="filter-bar" aria-label="Filtros">
155      <div class="search-wrapper">
156        ${icon('search')}
157        <input type="search" class="search-input" id="search-input" placeholder="Buscar por mazo, color o formato" autocomplete="off">
158      </div>
159      <select class="filter-select" id="filter-expansion" aria-label="Filtrar por seccion">
160        <option value="">Todas las secciones</option>
161        ${expansions.map(expansion => `<option value="${escapeHtml(expansion)}">${escapeHtml(expansion)}</option>`).join('')}
162      </select>
163      <select class="filter-select" id="filter-format" aria-label="Filtrar por formato">
164        <option value="">Todos los formatos</option>
165        ${formats.map(format => `<option value="${escapeHtml(format)}">${escapeHtml(format)}</option>`).join('')}
166      </select>
167      <select class="filter-select" id="filter-publish-status" aria-label="Filtrar por estado de publicacion">
168        <option value="published">Publicados</option>
169        <option value="scheduled">Programados</option>
170        <option value="all">Todos</option>
171      </select>
172      <select class="filter-select" id="filter-availability" aria-label="Filtrar por enlaces disponibles">
173        <option value="">Todos los enlaces</option>
174        <option value="video-deck">Video + mazo</option>
175        <option value="video">Con video</option>
176        <option value="deck">Con Untapped</option>
177        <option value="missing-deck">Sin lista</option>
178      </select>
179      <div class="filter-colors" aria-label="Filtrar por colores">
180        ${manaButtons.map(mana => `
181          <button class="filter-btn filter-btn--mana" data-color="${mana.code}" title="${mana.label}" aria-label="${mana.label}">
182            <span class="mana-dot mana-dot--lg" style="--mana:${MANA_COLORS[mana.code]}"></span>
183          </button>
184        `).join('')}
185      </div>
186      <button class="filter-btn filter-btn--clear" id="clear-filters">${icon('x')} Limpiar</button>
187      <button class="filter-btn filter-btn--refresh" id="refresh-data">${icon('refresh')} Actualizar</button>
188      <div class="filter-tags" id="active-tags"></div>
189    </section>`;
190}
191
192export function renderHeader() {
193  return `
194    <header class="header" id="header">
195      <a class="header__brand" href="#hero" aria-label="MagicBlogTK inicio">
196        <img class="brand-logo" src="/icons/creator-logo.png?v=18" alt="">
197        <span>MagicBlog<span>TK</span></span>
198      </a>
199      <div class="header__right">
200        <a class="live-badge" id="live-badge" href="https://www.twitch.tv/magicblogtk_" target="_blank" rel="noopener" hidden>
201          <span class="live-badge__dot" aria-hidden="true"></span>
202          <span class="live-badge__text">EN DIRECTO</span>
203        </a>
204        <span class="header__tagline">Mazos, videos y listas del creador</span>
205      </div>
206    </header>`;
207}
208
209export function renderHero(creatorLinks = getCreatorLinks()) {
210  const links = { ...getCreatorLinks(), ...creatorLinks };
211  return `
212    <section class="hero" id="hero">
213      <div class="hero__copy">
214        <p class="hero__label">Magic: The Gathering Arena</p>
215        <h1>Mazos MTG Arena</h1>
216        <p>Videos, listas de Untapped y redes oficiales de MagicBlogTK.</p>
217      </div>
218      <div class="hero__panel hero__panel--socials" aria-label="Redes oficiales de MagicBlogTK">
219        <div class="hero-social-grid" aria-label="Redes oficiales de MagicBlogTK">
220          ${renderSocialGrid(links)}
221        </div>
222      </div>
223    </section>`;
224}
225
226export function renderFooter(creatorLinks = getCreatorLinks()) {
227  const links = { ...getCreatorLinks(), ...creatorLinks };
228  return `
229    <footer class="footer">
230      <div>
231        <h2>MagicBlogTK</h2>
232        <p>Coleccion viva de mazos MTG Arena conectada a la hoja del creador.</p>
233      </div>
234      <div class="social-links">
235        ${Object.entries(links).map(([key, link]) => `
236          <a class="social-link social-link--${key}" href="${escapeHtml(link.url)}" target="_blank" rel="noopener">
237            ${icon(link.icon)}
238            <span>${escapeHtml(link.label)}</span>
239          </a>
240        `).join('')}
241      </div>
242    </footer>`;
243}
244
245export function renderViewTabs(active = 'decks') {
246  const tabs = [
247    { id: 'decks', label: 'Mazos' },
248    { id: 'rotation', label: 'Rotación' },
249    { id: 'banned', label: 'Baneadas' },
250  ];
251  return `
252    <div class="view-tabs" role="tablist" aria-label="Secciones">
253      ${tabs.map(tab => `
254        <button class="view-tab ${tab.id === active ? 'active' : ''}" type="button"
255          role="tab" aria-selected="${tab.id === active}" data-view="${tab.id}">${tab.label}</button>
256      `).join('')}
257    </div>`;
258}
259
260function renderRotationTable(items, showTheme) {
261  return `
262    <div class="info-table-wrap"><table class="info-table">
263      <thead><tr><th>Colección</th><th>Rota en</th>${showTheme ? '<th>Tema</th>' : ''}</tr></thead>
264      <tbody>
265        ${items.map(item => `
266          <tr>
267            <td>${escapeHtml(item.name)}</td>
268            <td><span class="year-pill">${escapeHtml(item.year || '—')}</span></td>
269            ${showTheme ? `<td class="info-dim">${escapeHtml(item.theme || '—')}</td>` : ''}
270          </tr>
271        `).join('')}
272      </tbody>
273    </table></div>`;
274}
275
276export function renderRotation({ legal, rotated }) {
277  return `
278    <div class="expansion-header" style="border-top:0;margin-top:6px">
279      <div>
280        <span class="expansion-header__kicker">Standard</span>
281        <h2>Rotación de formatos</h2>
282      </div>
283      <span class="expansion-count">${legal.length} legales</span>
284    </div>
285    <p class="info-lead">Cuándo rota cada colección fuera de Standard y su tema principal.</p>
286    ${renderRotationTable(legal, true)}
287    <div class="expansion-header">
288      <div>
289        <span class="expansion-header__kicker">Historial</span>
290        <h2>Ya rotados</h2>
291      </div>
292      <span class="expansion-count">${rotated.length} colecciones</span>
293    </div>
294    ${renderRotationTable(rotated, false)}`;
295}
296
297export function renderBanned(formats) {
298  const total = formats.reduce((sum, group) => sum + group.cards.length, 0);
299  const chips = [{ id: 'ALL', label: 'Todos' }, ...formats.map(group => ({ id: group.format, label: group.format }))];
300
301  return `
302    <div class="expansion-header" style="border-top:0;margin-top:6px">
303      <div>
304        <span class="expansion-header__kicker">Listas oficiales</span>
305        <h2>Cartas baneadas y restringidas</h2>
306      </div>
307      <span class="expansion-count" id="banned-results">${total} cartas</span>
308    </div>
309    <p class="info-lead">Busca una carta o elige un formato para ver dónde está prohibida.</p>
310    <div class="banned-controls">
311      <div class="search-wrapper banned-search-wrap">
312        ${icon('search')}
313        <input type="search" id="banned-search" class="search-input" placeholder="Buscar carta baneada" autocomplete="off">
314      </div>
315      <div class="banned-chips" id="banned-chips" role="tablist">
316        ${chips.map(chip => `
317          <button class="banned-chip ${chip.id === 'ALL' ? 'active' : ''}" type="button" data-format="${escapeHtml(chip.id)}">${escapeHtml(chip.label)}</button>
318        `).join('')}
319      </div>
320    </div>
321    <div class="banned-grid" id="banned-grid">
322      ${formats.map(group => `
323        <section class="banned-card" data-format="${escapeHtml(group.format)}">
324          <header class="banned-card__head">
325            <h3>${escapeHtml(group.format)}</h3>
326            <span class="banned-count">${group.cards.length}</span>
327          </header>
328          <ul class="banned-list">
329            ${group.cards.slice().sort((a, b) => a.localeCompare(b)).map(card => `<li>${escapeHtml(card)}</li>`).join('')}
330          </ul>
331        </section>
332      `).join('')}
333    </div>
334    <div class="empty-state banned-empty" id="banned-empty" hidden>
335      <h3>Sin resultados</h3>
336      <p>Ninguna carta coincide con la búsqueda.</p>
337    </div>`;
338}
339
340export function renderInfoState(message) {
341  return `<div class="empty-state"><h3>${escapeHtml(message)}</h3></div>`;
342}
343
344export function renderLoadingSkeletons(count = 9) {
345  return Array.from({ length: count }, () => `
346    <div class="deck-card deck-card--skeleton">
347      <div class="skeleton skeleton--short"></div>
348      <div class="skeleton skeleton--title"></div>
349      <div class="skeleton"></div>
350      <div class="skeleton skeleton--actions"></div>
351    </div>
352  `).join('');
353}
354
355export function renderEmptyState(query) {
356  return `
357    <div class="empty-state">
358      <h3>No hay resultados</h3>
359      <p>No encontramos mazos para "${escapeHtml(query)}". Prueba quitando algun filtro.</p>
360    </div>`;
361}
362
363let toastTimeout = null;
364export function showToast(message, type = 'success') {
365  let container = document.getElementById('toast-container');
366  if (!container) {
367    container = document.createElement('div');
368    container.id = 'toast-container';
369    container.className = 'toast-container';
370    document.body.appendChild(container);
371  }
372
373  container.innerHTML = `<div class="toast toast--${type}">${escapeHtml(message)}</div>`;
374  clearTimeout(toastTimeout);
375  toastTimeout = setTimeout(() => {
376    container.innerHTML = '';
377  }, 2600);
378}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.