@thebattlearena/corner 0.9.1-beta.70 → 0.10.1-beta.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -26
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/ring/agents/cornerman.md +32 -6
- package/ring/agents/rudo.md +2 -1
- package/ring/agents/wrestler.md +19 -4
- package/ring/hooks/hooks.json +42 -0
- package/ring/scripts/bout-stats-render.mjs +366 -0
- package/ring/scripts/bout-stats.mjs +360 -0
- package/ring/scripts/brief-build.mjs +130 -0
- package/ring/scripts/brief-guard.mjs +154 -0
- package/ring/scripts/brief.mjs +272 -0
- package/ring/scripts/compact-hook.mjs +110 -0
- package/ring/scripts/config.mjs +25 -1
- package/ring/scripts/contract-endpoints.mjs +163 -0
- package/ring/scripts/contract-preview.mjs +34 -14
- package/ring/scripts/fight.mjs +27 -0
- package/ring/scripts/findings.mjs +82 -3
- package/ring/scripts/format-gate.mjs +723 -0
- package/ring/scripts/format-worker.mjs +120 -0
- package/ring/scripts/front-routes.mjs +345 -0
- package/ring/scripts/judges-card.mjs +324 -3
- package/ring/scripts/lookout.mjs +22 -4
- package/ring/scripts/mask-guard.mjs +34 -1
- package/ring/scripts/page-embed.mjs +112 -0
- package/ring/scripts/play-by-play.mjs +155 -7
- package/ring/scripts/preparation-collect.mjs +177 -0
- package/ring/scripts/preparation.mjs +547 -0
- package/ring/scripts/round-bell.mjs +9 -2
- package/ring/scripts/rule-ids.mjs +124 -0
- package/ring/scripts/tech-plan-log.mjs +81 -0
- package/ring/scripts/tech-plan.mjs +191 -0
- package/ring/scripts/verdicts-log.mjs +75 -0
- package/ring/scripts/verdicts.mjs +205 -0
- package/ring/skills/play-by-play/SKILL.md +235 -33
- package/ring/skills/play-by-play/templates/blocks.css +196 -0
- package/ring/skills/play-by-play/templates/chronicle.html +358 -55
- package/ring/skills/play-by-play/templates/endpoints-view.js +1153 -0
- package/ring/skills/play-by-play/templates/interview-form.js +516 -0
- package/ring/skills/play-by-play/templates/now-panel.js +719 -0
- package/ring/skills/referee/SKILL.md +181 -56
- package/ring/skills/referee/references/api-diagram.md +56 -0
- package/ring/skills/referee/references/precedents.md +11 -0
- package/ring/skills/referee/references/rulebook.md +97 -6
- package/ring/skills/referee/templates/api-diagram.html +115 -0
- package/ring/skills/referee/templates/cornerman-batch.md +17 -11
- package/ring/skills/referee/templates/cornerman-concept.md +28 -30
- package/ring/skills/referee/templates/cornerman-range.md +16 -11
- package/ring/skills/referee/templates/escalation.md +7 -1
- package/ring/skills/referee/templates/round-bell-body.md +4 -0
- package/ring/skills/referee/templates/wrestler-brief.md +28 -41
- package/ring/skills/referee/templates/wrestler-cutoff.md +4 -0
- package/ring/skills/referee/templates/wrestler-resume.md +4 -0
- package/ring/skills/sparring/SKILL.md +13 -4
- package/ring/skills/sparring/templates/contrato.md +29 -0
- package/ring/skills/sparring/templates/rudo-brief.md +7 -0
- package/ring/skills/title-defense/SKILL.md +2 -2
- package/ring/skills/weigh-in/SKILL.md +6 -0
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
// «Qué necesita de ti ahora», the dark panel on top of the chronicle (`section[data-ring-now]`):
|
|
2
|
+
// what the judge has to do, counted live from the page's bus (`window.ringPage`, which
|
|
3
|
+
// interview-form.js and endpoints-view.js fill from `respuestas` and `veredictos`) and from each
|
|
4
|
+
// bout's line the skill wrote (`.now-bout[data-pr][data-label][data-frozen]`). Per bout it shows
|
|
5
|
+
// the next question and the next endpoints or routes with their controls, so the judge answers and
|
|
6
|
+
// judges without going down; «Ver la cola» shows the rest. «Aceptar las N recomendaciones del
|
|
7
|
+
// réferi» saves the recommended option of every unanswered question only after a dialog that lists
|
|
8
|
+
// each one (Julio: nothing is saved without him knowing). With nothing to do, the panel says who
|
|
9
|
+
// has the ball and the latest news (`.now-ball`, `.now-news`). It also runs «Abrir todo» of «La
|
|
10
|
+
// crónica» and opens the folded stage an in-page link points into.
|
|
11
|
+
(() => {
|
|
12
|
+
const ring = (window.ringPage ??= {
|
|
13
|
+
questions: new Map(),
|
|
14
|
+
boards: new Map(),
|
|
15
|
+
scales: {},
|
|
16
|
+
changed() {
|
|
17
|
+
document.dispatchEvent(new CustomEvent('ring:change'));
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const el = (tag, attrs = {}, ...children) => {
|
|
22
|
+
const node = document.createElement(tag);
|
|
23
|
+
Object.entries(attrs).forEach(([key, value]) => {
|
|
24
|
+
if (value === undefined || value === null || value === false) return;
|
|
25
|
+
if (key === 'text') return void (node.textContent = value);
|
|
26
|
+
if (key.startsWith('on'))
|
|
27
|
+
return void node.addEventListener(key.slice(2), value);
|
|
28
|
+
node.setAttribute(key, value === true ? '' : value);
|
|
29
|
+
});
|
|
30
|
+
children
|
|
31
|
+
.flat(Infinity)
|
|
32
|
+
.filter(
|
|
33
|
+
(child) => child !== null && child !== undefined && child !== false,
|
|
34
|
+
)
|
|
35
|
+
.forEach((child) => node.append(child));
|
|
36
|
+
return node;
|
|
37
|
+
};
|
|
38
|
+
/** replaceChildren that flattens arrays and skips null, undefined and false, like `el`. */
|
|
39
|
+
const put = (node, ...children) =>
|
|
40
|
+
node.replaceChildren(
|
|
41
|
+
...children
|
|
42
|
+
.flat(Infinity)
|
|
43
|
+
.filter((child) => child !== null && child !== undefined && child !== false),
|
|
44
|
+
);
|
|
45
|
+
const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
46
|
+
const list = (items) =>
|
|
47
|
+
items.length < 2
|
|
48
|
+
? items.join('')
|
|
49
|
+
: `${items.slice(0, -1).join(', ')} y ${items.at(-1)}`;
|
|
50
|
+
|
|
51
|
+
// «La crónica»: «Abrir todo» and the stage a link points into.
|
|
52
|
+
document.querySelectorAll('[data-ring-open-all]').forEach((button) => {
|
|
53
|
+
button.addEventListener('click', () => {
|
|
54
|
+
const stages = [...document.querySelectorAll('details.stage')];
|
|
55
|
+
const open = !stages.every((stage) => stage.open);
|
|
56
|
+
stages.forEach((stage) => (stage.open = open));
|
|
57
|
+
button.textContent = open ? 'Cerrar todo' : 'Abrir todo';
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
const reveal = (target) => {
|
|
61
|
+
for (let node = target; node; node = node.parentElement)
|
|
62
|
+
if (node.tagName === 'DETAILS') node.open = true;
|
|
63
|
+
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
64
|
+
};
|
|
65
|
+
document.addEventListener('click', (event) => {
|
|
66
|
+
const link = event.target.closest?.('a[href^="#"]');
|
|
67
|
+
const target =
|
|
68
|
+
link && link.getAttribute('href').length > 1
|
|
69
|
+
? document.getElementById(
|
|
70
|
+
decodeURIComponent(link.getAttribute('href').slice(1)),
|
|
71
|
+
)
|
|
72
|
+
: null;
|
|
73
|
+
if (!target) return;
|
|
74
|
+
event.preventDefault();
|
|
75
|
+
history.replaceState(null, '', link.getAttribute('href'));
|
|
76
|
+
reveal(target);
|
|
77
|
+
});
|
|
78
|
+
if (location.hash.length > 1) {
|
|
79
|
+
const target = document.getElementById(
|
|
80
|
+
decodeURIComponent(location.hash.slice(1)),
|
|
81
|
+
);
|
|
82
|
+
if (target) setTimeout(() => reveal(target), 300);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const panel = document.querySelector('section[data-ring-now]');
|
|
86
|
+
if (!panel) return;
|
|
87
|
+
// The ball and the news of the whole fight sit outside the bouts; a bout's own sit inside its
|
|
88
|
+
// div.now-bout and are kept (moved, never lost) into its card on every repaint.
|
|
89
|
+
const fightNotes = [...panel.querySelectorAll('.now-ball, .now-news')].filter(
|
|
90
|
+
(node) => !node.closest('.now-bout'),
|
|
91
|
+
);
|
|
92
|
+
const bouts = [...panel.querySelectorAll('.now-bout[data-pr]')].map(
|
|
93
|
+
(node) => ({
|
|
94
|
+
node,
|
|
95
|
+
pr: Number(node.dataset.pr),
|
|
96
|
+
label: node.dataset.label || `#${node.dataset.pr}`,
|
|
97
|
+
frozen: (node.dataset.frozen || '').trim(),
|
|
98
|
+
notes: [...node.querySelectorAll('.now-ball, .now-news')],
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
101
|
+
/** Where a bout starts in «La crónica»: its h3.bout in an open stage, else its first one. */
|
|
102
|
+
const boutTarget = (pr) => {
|
|
103
|
+
const heads = [
|
|
104
|
+
...document.querySelectorAll(
|
|
105
|
+
`h3.bout[data-pr="${pr}"], h3.bout[id="bout-${pr}"], h3.bout[id^="bout-${pr}-"]`,
|
|
106
|
+
),
|
|
107
|
+
];
|
|
108
|
+
return heads.find((head) => head.closest('details.stage[open]')) ?? heads[0];
|
|
109
|
+
};
|
|
110
|
+
const head = el('div', { class: 'now-head' });
|
|
111
|
+
const counters = el('div', { class: 'now-counters' });
|
|
112
|
+
const actions = el('div', { class: 'now-actions' });
|
|
113
|
+
const boutsBox =
|
|
114
|
+
panel.querySelector('.now-bouts') ?? el('div', { class: 'now-bouts' });
|
|
115
|
+
const status = el('div', { class: 'now-status' }, fightNotes);
|
|
116
|
+
panel.prepend(head, counters);
|
|
117
|
+
panel.append(boutsBox, actions, status);
|
|
118
|
+
|
|
119
|
+
/** An image drop for the panel's forms; keeps the uploaded asset ids in `attached`. */
|
|
120
|
+
function attacher(attached, say) {
|
|
121
|
+
const file = el('input', {
|
|
122
|
+
type: 'file',
|
|
123
|
+
accept: 'image/*',
|
|
124
|
+
multiple: true,
|
|
125
|
+
hidden: true,
|
|
126
|
+
});
|
|
127
|
+
const thumbs = el('div', { class: 'thumbs' });
|
|
128
|
+
const add = async (files) => {
|
|
129
|
+
for (const picked of files) {
|
|
130
|
+
say(`Subiendo ${picked.name}…`);
|
|
131
|
+
try {
|
|
132
|
+
if (!ring.upload) throw new Error('Esta vista no puede adjuntar');
|
|
133
|
+
const up = await ring.upload(picked);
|
|
134
|
+
attached.push(up.id);
|
|
135
|
+
thumbs.append(el('img', { src: up.url, alt: picked.name }));
|
|
136
|
+
say('');
|
|
137
|
+
} catch (error) {
|
|
138
|
+
say(error.message);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
file.addEventListener('change', () => add([...file.files]));
|
|
143
|
+
const drop = el('div', {
|
|
144
|
+
class: 'drop',
|
|
145
|
+
tabindex: '0',
|
|
146
|
+
text: 'Arrastra imágenes aquí o haz clic para adjuntar',
|
|
147
|
+
onclick: () => file.click(),
|
|
148
|
+
onkeydown: (event) => ['Enter', ' '].includes(event.key) && file.click(),
|
|
149
|
+
ondragover: (event) => {
|
|
150
|
+
event.preventDefault();
|
|
151
|
+
drop.classList.add('over');
|
|
152
|
+
},
|
|
153
|
+
ondragleave: () => drop.classList.remove('over'),
|
|
154
|
+
ondrop: (event) => {
|
|
155
|
+
event.preventDefault();
|
|
156
|
+
drop.classList.remove('over');
|
|
157
|
+
add([...event.dataTransfer.files]);
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
return [drop, file, thumbs];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Each form is built once and kept, so typing, a picked option or an image survive a repaint.
|
|
164
|
+
const questionForms = new Map();
|
|
165
|
+
function questionForm(question) {
|
|
166
|
+
if (questionForms.has(question.id)) return questionForms.get(question.id);
|
|
167
|
+
const name = `now-${question.id}`;
|
|
168
|
+
const say = (text) => (state.textContent = text);
|
|
169
|
+
const attached = [];
|
|
170
|
+
const note = el('textarea', {
|
|
171
|
+
placeholder: 'Comentario para el réferi (opcional)',
|
|
172
|
+
});
|
|
173
|
+
const extra = el(
|
|
174
|
+
'div',
|
|
175
|
+
{ class: 'now-extra', hidden: true },
|
|
176
|
+
note,
|
|
177
|
+
attacher(attached, say),
|
|
178
|
+
);
|
|
179
|
+
const state = el('span', { class: 'status', role: 'status' });
|
|
180
|
+
const save = el('button', {
|
|
181
|
+
type: 'button',
|
|
182
|
+
class: 'save',
|
|
183
|
+
text: 'Guardar respuesta',
|
|
184
|
+
disabled: true,
|
|
185
|
+
onclick: async () => {
|
|
186
|
+
const picked = form.querySelector(`input[name="${name}"]:checked`);
|
|
187
|
+
if (!picked) return say('Elige una opción');
|
|
188
|
+
const option = question.options.find(
|
|
189
|
+
(entry) => entry.key === picked.value,
|
|
190
|
+
);
|
|
191
|
+
save.disabled = true;
|
|
192
|
+
say('Guardando…');
|
|
193
|
+
try {
|
|
194
|
+
await question.save({
|
|
195
|
+
choice: option.key,
|
|
196
|
+
text: option.text(),
|
|
197
|
+
note: note.value,
|
|
198
|
+
attachments: attached,
|
|
199
|
+
});
|
|
200
|
+
say('Guardada');
|
|
201
|
+
} catch (error) {
|
|
202
|
+
say(error.message);
|
|
203
|
+
save.disabled = false;
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
const form = el(
|
|
208
|
+
'div',
|
|
209
|
+
{ class: 'now-q', id: `now-${question.id}` },
|
|
210
|
+
el(
|
|
211
|
+
'div',
|
|
212
|
+
{ class: 'now-q-head' },
|
|
213
|
+
el('b', { text: question.title }),
|
|
214
|
+
question.time ? el('time', { text: question.time }) : null,
|
|
215
|
+
question.hasContext
|
|
216
|
+
? el('button', {
|
|
217
|
+
type: 'button',
|
|
218
|
+
class: 'more',
|
|
219
|
+
text: 'Ver más información',
|
|
220
|
+
onclick: () =>
|
|
221
|
+
ring.openContext?.(question.title, question.context),
|
|
222
|
+
})
|
|
223
|
+
: null,
|
|
224
|
+
),
|
|
225
|
+
el(
|
|
226
|
+
'div',
|
|
227
|
+
{
|
|
228
|
+
class: 'now-opts',
|
|
229
|
+
role: 'radiogroup',
|
|
230
|
+
onchange: () => (save.disabled = false),
|
|
231
|
+
},
|
|
232
|
+
question.options.map((option) =>
|
|
233
|
+
el(
|
|
234
|
+
'label',
|
|
235
|
+
{ class: `now-opt${option.rec ? ' rec' : ''}` },
|
|
236
|
+
el('input', { type: 'radio', name, value: option.key }),
|
|
237
|
+
el('i', { text: option.key }),
|
|
238
|
+
el('span', { text: option.text() }),
|
|
239
|
+
option.rec ? el('em', { text: 'Recomienda el réferi' }) : null,
|
|
240
|
+
),
|
|
241
|
+
),
|
|
242
|
+
),
|
|
243
|
+
el(
|
|
244
|
+
'div',
|
|
245
|
+
{ class: 'now-row q-actions' },
|
|
246
|
+
save,
|
|
247
|
+
el('button', {
|
|
248
|
+
type: 'button',
|
|
249
|
+
class: 'ghost',
|
|
250
|
+
text: 'Comentario o imagen',
|
|
251
|
+
onclick: () => (extra.hidden = !extra.hidden),
|
|
252
|
+
}),
|
|
253
|
+
el('a', {
|
|
254
|
+
href: `#${question.id}`,
|
|
255
|
+
class: 'jump',
|
|
256
|
+
text: 'En la crónica',
|
|
257
|
+
}),
|
|
258
|
+
state,
|
|
259
|
+
),
|
|
260
|
+
extra,
|
|
261
|
+
);
|
|
262
|
+
questionForms.set(question.id, form);
|
|
263
|
+
return form;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const verdictRows = new Map();
|
|
267
|
+
function verdictRow(item) {
|
|
268
|
+
const known = verdictRows.get(item.key);
|
|
269
|
+
if (known) {
|
|
270
|
+
known.item = item;
|
|
271
|
+
known.node.classList.toggle('stale', item.verdict === 'stale');
|
|
272
|
+
return known.node;
|
|
273
|
+
}
|
|
274
|
+
const entry = { item };
|
|
275
|
+
const attached = [];
|
|
276
|
+
const say = (text) => (state.textContent = text);
|
|
277
|
+
const state = el('span', { class: 'status', role: 'status' });
|
|
278
|
+
const comment = el('textarea', {
|
|
279
|
+
placeholder: '¿Qué hay que mejorar? (obligatorio)',
|
|
280
|
+
});
|
|
281
|
+
const send = async (verdict) => {
|
|
282
|
+
say('Guardando…');
|
|
283
|
+
try {
|
|
284
|
+
await entry.item.save(verdict, comment.value, attached);
|
|
285
|
+
say('Guardado');
|
|
286
|
+
} catch (error) {
|
|
287
|
+
say(error.message);
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
const improve = el(
|
|
291
|
+
'div',
|
|
292
|
+
{ class: 'now-extra', hidden: true },
|
|
293
|
+
comment,
|
|
294
|
+
attacher(attached, say),
|
|
295
|
+
el(
|
|
296
|
+
'div',
|
|
297
|
+
{ class: 'now-row' },
|
|
298
|
+
el('button', {
|
|
299
|
+
type: 'button',
|
|
300
|
+
class: 'save',
|
|
301
|
+
text: 'Guardar «se mejora»',
|
|
302
|
+
onclick: () => send('improve'),
|
|
303
|
+
}),
|
|
304
|
+
),
|
|
305
|
+
);
|
|
306
|
+
entry.node = el(
|
|
307
|
+
'div',
|
|
308
|
+
{ class: `now-ep${item.verdict === 'stale' ? ' stale' : ''}` },
|
|
309
|
+
el(
|
|
310
|
+
'div',
|
|
311
|
+
{ class: 'now-ep-line' },
|
|
312
|
+
el('b', { class: `m ${item.chipClass}`, text: item.chip }),
|
|
313
|
+
el('a', { href: `#${item.key}`, class: 'path', text: item.path }),
|
|
314
|
+
el('span', { class: 'was', text: 'desactualizado' }),
|
|
315
|
+
el(
|
|
316
|
+
'span',
|
|
317
|
+
{ class: 'now-ep-buttons' },
|
|
318
|
+
el('button', {
|
|
319
|
+
type: 'button',
|
|
320
|
+
class: 'approve',
|
|
321
|
+
text: '✓ Se aprueba',
|
|
322
|
+
onclick: () => send('approved'),
|
|
323
|
+
}),
|
|
324
|
+
el('button', {
|
|
325
|
+
type: 'button',
|
|
326
|
+
class: 'improve-btn',
|
|
327
|
+
text: '✎ Se mejora',
|
|
328
|
+
onclick: () => (improve.hidden = !improve.hidden),
|
|
329
|
+
}),
|
|
330
|
+
),
|
|
331
|
+
),
|
|
332
|
+
state,
|
|
333
|
+
improve,
|
|
334
|
+
);
|
|
335
|
+
verdictRows.set(item.key, entry);
|
|
336
|
+
return entry.node;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const expanded = new Set();
|
|
340
|
+
const pendingOf = (pr) => {
|
|
341
|
+
const questions = [...ring.questions.values()]
|
|
342
|
+
.filter((question) => question.pr === pr)
|
|
343
|
+
.sort(
|
|
344
|
+
(a, b) =>
|
|
345
|
+
a.esc.localeCompare(b.esc, 'es', { numeric: true }) || a.n - b.n,
|
|
346
|
+
);
|
|
347
|
+
const boards = [...ring.boards.values()].filter((board) => board.pr === pr);
|
|
348
|
+
const items = boards.flatMap((board) =>
|
|
349
|
+
board.items
|
|
350
|
+
.filter((item) => ['none', 'stale'].includes(item.verdict))
|
|
351
|
+
.map((item) => ({ ...item, noun: board.noun })),
|
|
352
|
+
);
|
|
353
|
+
return {
|
|
354
|
+
questions,
|
|
355
|
+
open: questions.filter((question) => !question.answered),
|
|
356
|
+
boards,
|
|
357
|
+
items,
|
|
358
|
+
approved: boards.reduce((sum, board) => sum + board.approved, 0),
|
|
359
|
+
total: boards.reduce((sum, board) => sum + board.total, 0),
|
|
360
|
+
};
|
|
361
|
+
};
|
|
362
|
+
const nounCount = (items) => {
|
|
363
|
+
const byNoun = {};
|
|
364
|
+
items.forEach((item) => (byNoun[item.noun] = (byNoun[item.noun] ?? 0) + 1));
|
|
365
|
+
return Object.entries(byNoun).map(([noun, n]) =>
|
|
366
|
+
plural(n, noun === 'rutas' ? 'ruta' : 'endpoint', noun),
|
|
367
|
+
);
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
function boutCard(bout, pending) {
|
|
371
|
+
const scale = ring.scales[bout.pr];
|
|
372
|
+
const red = !scale?.green;
|
|
373
|
+
const showAll = expanded.has(bout.pr);
|
|
374
|
+
const questions = showAll ? pending.open : pending.open.slice(0, 1);
|
|
375
|
+
const items = showAll ? pending.items : pending.items.slice(0, 2);
|
|
376
|
+
const restQuestions = pending.open.length - questions.length;
|
|
377
|
+
const restItems = pending.items.length - items.length;
|
|
378
|
+
const answered = pending.questions.length - pending.open.length;
|
|
379
|
+
const nothing = !pending.open.length && !pending.items.length;
|
|
380
|
+
bout.node.className = `now-bout${nothing ? ' clear' : ''}`;
|
|
381
|
+
put(bout.node,
|
|
382
|
+
el(
|
|
383
|
+
'div',
|
|
384
|
+
{ class: 'now-bout-head' },
|
|
385
|
+
el(
|
|
386
|
+
'h3',
|
|
387
|
+
{},
|
|
388
|
+
el('a', {
|
|
389
|
+
href: '#',
|
|
390
|
+
text: `#${bout.pr}`,
|
|
391
|
+
onclick: (event) => {
|
|
392
|
+
event.preventDefault();
|
|
393
|
+
const target = boutTarget(bout.pr);
|
|
394
|
+
if (target) reveal(target);
|
|
395
|
+
},
|
|
396
|
+
}),
|
|
397
|
+
` · ${bout.label}`,
|
|
398
|
+
),
|
|
399
|
+
pending.open.length
|
|
400
|
+
? el('span', {
|
|
401
|
+
class: 'now-of',
|
|
402
|
+
text: `pregunta ${answered + 1} de ${pending.questions.length}`,
|
|
403
|
+
})
|
|
404
|
+
: null,
|
|
405
|
+
scale
|
|
406
|
+
? el('span', {
|
|
407
|
+
class: `now-scale ${red ? 'red' : 'green'}`,
|
|
408
|
+
text: `${red ? 'No da el peso' : 'Da el peso'} · ${scale.approved}/${scale.total}`,
|
|
409
|
+
})
|
|
410
|
+
: null,
|
|
411
|
+
),
|
|
412
|
+
bout.frozen
|
|
413
|
+
? el('p', { class: 'now-frozen', text: `Congelada: ${bout.frozen}` })
|
|
414
|
+
: null,
|
|
415
|
+
nothing
|
|
416
|
+
? el('p', {
|
|
417
|
+
class: 'now-none',
|
|
418
|
+
text:
|
|
419
|
+
scale &&
|
|
420
|
+
!scale.green &&
|
|
421
|
+
scale.total &&
|
|
422
|
+
scale.approved === scale.total
|
|
423
|
+
? 'Todo aprobado en la página; falta que apruebes el contrato con el réferi (approve-contract).'
|
|
424
|
+
: 'Nada pendiente de ti en esta pelea.',
|
|
425
|
+
})
|
|
426
|
+
: null,
|
|
427
|
+
questions.map(questionForm),
|
|
428
|
+
pending.boards.length && items.length
|
|
429
|
+
? el('h4', {
|
|
430
|
+
class: 'now-sub',
|
|
431
|
+
text: `Veredictos · ${pending.approved} de ${pending.total}`,
|
|
432
|
+
})
|
|
433
|
+
: null,
|
|
434
|
+
items.map(verdictRow),
|
|
435
|
+
restQuestions || restItems || showAll
|
|
436
|
+
? el(
|
|
437
|
+
'button',
|
|
438
|
+
{
|
|
439
|
+
type: 'button',
|
|
440
|
+
class: 'now-queue',
|
|
441
|
+
'aria-expanded': showAll ? 'true' : 'false',
|
|
442
|
+
onclick: () => {
|
|
443
|
+
if (showAll) expanded.delete(bout.pr);
|
|
444
|
+
else expanded.add(bout.pr);
|
|
445
|
+
paint();
|
|
446
|
+
},
|
|
447
|
+
},
|
|
448
|
+
el('span', { class: 'chev', text: showAll ? '▾' : '▸' }),
|
|
449
|
+
el('b', {
|
|
450
|
+
text: showAll
|
|
451
|
+
? 'Ocultar la cola'
|
|
452
|
+
: `Siguientes ${list(
|
|
453
|
+
[
|
|
454
|
+
restQuestions
|
|
455
|
+
? plural(restQuestions, 'pregunta', 'preguntas')
|
|
456
|
+
: '',
|
|
457
|
+
...nounCount(pending.items.slice(items.length)),
|
|
458
|
+
].filter(Boolean),
|
|
459
|
+
)}`,
|
|
460
|
+
}),
|
|
461
|
+
showAll ? null : el('span', { class: 'muted', text: 'en la cola' }),
|
|
462
|
+
el('span', { class: 'go', text: showAll ? '' : 'Ver la cola ▸' }),
|
|
463
|
+
)
|
|
464
|
+
: null,
|
|
465
|
+
bout.notes.length
|
|
466
|
+
? el('div', { class: 'now-bout-status' }, bout.notes)
|
|
467
|
+
: null,
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function counter(n, label, sub, tone) {
|
|
472
|
+
return el(
|
|
473
|
+
'div',
|
|
474
|
+
{ class: `now-count ${n ? tone : 'zero'}` },
|
|
475
|
+
el('b', { text: String(n) }),
|
|
476
|
+
el('span', { text: label }),
|
|
477
|
+
el('small', { text: sub }),
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const recommended = () =>
|
|
482
|
+
[...ring.questions.values()]
|
|
483
|
+
.filter((question) => !question.answered)
|
|
484
|
+
.map((question) => ({
|
|
485
|
+
question,
|
|
486
|
+
option: question.options.find((option) => option.rec),
|
|
487
|
+
}))
|
|
488
|
+
.filter((entry) => entry.option);
|
|
489
|
+
|
|
490
|
+
let dialog = null;
|
|
491
|
+
function confirmAccept() {
|
|
492
|
+
const entries = recommended();
|
|
493
|
+
if (!entries.length) return;
|
|
494
|
+
dialog?.remove();
|
|
495
|
+
const say = el('p', { class: 'status', role: 'status' });
|
|
496
|
+
const boxes = entries.map((entry) =>
|
|
497
|
+
el('input', { type: 'checkbox', checked: true }),
|
|
498
|
+
);
|
|
499
|
+
const go = el('button', { type: 'button', class: 'save' });
|
|
500
|
+
const label = () => {
|
|
501
|
+
const n = boxes.filter((box) => box.checked).length;
|
|
502
|
+
go.textContent = `Guardar ${plural(n, 'respuesta', 'respuestas')}`;
|
|
503
|
+
go.disabled = !n;
|
|
504
|
+
};
|
|
505
|
+
dialog = el(
|
|
506
|
+
'dialog',
|
|
507
|
+
{
|
|
508
|
+
class: 'ctx-dialog now-confirm',
|
|
509
|
+
'aria-labelledby': 'now-confirm-title',
|
|
510
|
+
},
|
|
511
|
+
el(
|
|
512
|
+
'div',
|
|
513
|
+
{ class: 'ctx-head' },
|
|
514
|
+
el('h3', {
|
|
515
|
+
id: 'now-confirm-title',
|
|
516
|
+
text: 'Aceptar las recomendaciones del réferi',
|
|
517
|
+
}),
|
|
518
|
+
el('button', {
|
|
519
|
+
type: 'button',
|
|
520
|
+
class: 'ctx-close',
|
|
521
|
+
'aria-label': 'Cerrar',
|
|
522
|
+
text: '×',
|
|
523
|
+
onclick: () => dialog.close(),
|
|
524
|
+
}),
|
|
525
|
+
),
|
|
526
|
+
el(
|
|
527
|
+
'div',
|
|
528
|
+
{ class: 'ctx-body' },
|
|
529
|
+
el('p', {
|
|
530
|
+
text: 'Se guardará esta respuesta en cada pregunta marcada, como si la eligieras una por una. Desmarca las que quieras contestar tú.',
|
|
531
|
+
}),
|
|
532
|
+
el(
|
|
533
|
+
'ul',
|
|
534
|
+
{ class: 'now-confirm-list' },
|
|
535
|
+
entries.map(({ question, option }, index) =>
|
|
536
|
+
el(
|
|
537
|
+
'li',
|
|
538
|
+
{},
|
|
539
|
+
el(
|
|
540
|
+
'label',
|
|
541
|
+
{},
|
|
542
|
+
boxes[index],
|
|
543
|
+
el(
|
|
544
|
+
'span',
|
|
545
|
+
{},
|
|
546
|
+
el('b', { text: `#${question.pr} · ${question.title}` }),
|
|
547
|
+
el('br'),
|
|
548
|
+
`${option.key}: ${option.text()}`,
|
|
549
|
+
),
|
|
550
|
+
),
|
|
551
|
+
),
|
|
552
|
+
),
|
|
553
|
+
),
|
|
554
|
+
el(
|
|
555
|
+
'div',
|
|
556
|
+
{ class: 'now-row' },
|
|
557
|
+
go,
|
|
558
|
+
el('button', {
|
|
559
|
+
type: 'button',
|
|
560
|
+
class: 'ghost',
|
|
561
|
+
text: 'Cancelar',
|
|
562
|
+
onclick: () => dialog.close(),
|
|
563
|
+
}),
|
|
564
|
+
),
|
|
565
|
+
say,
|
|
566
|
+
),
|
|
567
|
+
);
|
|
568
|
+
boxes.forEach((box) => box.addEventListener('change', label));
|
|
569
|
+
label();
|
|
570
|
+
go.addEventListener('click', async () => {
|
|
571
|
+
go.disabled = true;
|
|
572
|
+
let saved = 0;
|
|
573
|
+
const failed = [];
|
|
574
|
+
for (const [index, { question, option }] of entries.entries()) {
|
|
575
|
+
if (!boxes[index].checked) continue;
|
|
576
|
+
say.textContent = `Guardando ${saved + 1}…`;
|
|
577
|
+
try {
|
|
578
|
+
await question.save({ choice: option.key, text: option.text() });
|
|
579
|
+
saved += 1;
|
|
580
|
+
} catch (error) {
|
|
581
|
+
failed.push(`${question.title}: ${error.message}`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
say.textContent = failed.length
|
|
585
|
+
? `Guardadas ${saved}; no se guardaron ${failed.length}: ${failed.join(' · ')}`
|
|
586
|
+
: `Guardadas ${saved}.`;
|
|
587
|
+
if (!failed.length) setTimeout(() => dialog.close(), 900);
|
|
588
|
+
else go.disabled = false;
|
|
589
|
+
});
|
|
590
|
+
document.body.append(dialog);
|
|
591
|
+
dialog.addEventListener(
|
|
592
|
+
'click',
|
|
593
|
+
(event) => event.target === dialog && dialog.close(),
|
|
594
|
+
);
|
|
595
|
+
dialog.showModal();
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
let queued = false;
|
|
599
|
+
function paint() {
|
|
600
|
+
queued = false;
|
|
601
|
+
const perBout = bouts.map((bout) => ({
|
|
602
|
+
bout,
|
|
603
|
+
pending: pendingOf(bout.pr),
|
|
604
|
+
}));
|
|
605
|
+
const open = perBout.flatMap(({ pending }) => pending.open);
|
|
606
|
+
const items = perBout.flatMap(({ pending }) => pending.items);
|
|
607
|
+
const red = bouts.filter(
|
|
608
|
+
(bout) => ring.scales[bout.pr] && !ring.scales[bout.pr].green,
|
|
609
|
+
);
|
|
610
|
+
const frozen = bouts.filter((bout) => bout.frozen);
|
|
611
|
+
const busy = open.length + items.length > 0;
|
|
612
|
+
panel.classList.toggle('idle', !busy);
|
|
613
|
+
const oldest = open
|
|
614
|
+
.map((question) => question.time)
|
|
615
|
+
.filter(Boolean)
|
|
616
|
+
.sort()[0];
|
|
617
|
+
put(head,
|
|
618
|
+
el(
|
|
619
|
+
'div',
|
|
620
|
+
{},
|
|
621
|
+
el('span', {
|
|
622
|
+
class: 'now-kicker',
|
|
623
|
+
text: busy ? 'Tu turno, juez' : 'Sin pendientes del juez',
|
|
624
|
+
}),
|
|
625
|
+
el('h2', {
|
|
626
|
+
text: busy ? 'Qué necesita de ti ahora' : 'Nada que pedirte ahora',
|
|
627
|
+
}),
|
|
628
|
+
),
|
|
629
|
+
busy
|
|
630
|
+
? el(
|
|
631
|
+
'div',
|
|
632
|
+
{ class: 'now-wait' },
|
|
633
|
+
oldest
|
|
634
|
+
? el('span', { text: `el réferi espera desde las ${oldest}` })
|
|
635
|
+
: null,
|
|
636
|
+
open.length
|
|
637
|
+
? el('em', {
|
|
638
|
+
text: 'sin tu respuesta, el luchador no recibe rebote',
|
|
639
|
+
})
|
|
640
|
+
: null,
|
|
641
|
+
)
|
|
642
|
+
: null,
|
|
643
|
+
);
|
|
644
|
+
const byBout = (pick) =>
|
|
645
|
+
perBout
|
|
646
|
+
.filter(({ pending }) => pick(pending).length)
|
|
647
|
+
.map(
|
|
648
|
+
({ bout, pending }) =>
|
|
649
|
+
`${pick(pending).length} en ${bout.label.toLowerCase()}`,
|
|
650
|
+
);
|
|
651
|
+
put(counters,
|
|
652
|
+
counter(
|
|
653
|
+
open.length,
|
|
654
|
+
'Preguntas sin respuesta',
|
|
655
|
+
list(byBout((p) => p.open)) || 'ninguna',
|
|
656
|
+
'red',
|
|
657
|
+
),
|
|
658
|
+
counter(
|
|
659
|
+
items.length,
|
|
660
|
+
'Veredictos pendientes',
|
|
661
|
+
list(nounCount(items)) || 'ninguno',
|
|
662
|
+
'red',
|
|
663
|
+
),
|
|
664
|
+
counter(
|
|
665
|
+
red.length,
|
|
666
|
+
'Básculas en rojo',
|
|
667
|
+
red.length
|
|
668
|
+
? `no da el peso: ${list(red.map((bout) => `#${bout.pr}`))}`
|
|
669
|
+
: 'todas dan el peso',
|
|
670
|
+
'gold',
|
|
671
|
+
),
|
|
672
|
+
counter(
|
|
673
|
+
frozen.length,
|
|
674
|
+
frozen.length === 1 ? 'Pelea congelada' : 'Peleas congeladas',
|
|
675
|
+
frozen.length
|
|
676
|
+
? frozen.map((bout) => `#${bout.pr} ${bout.frozen}`).join(' · ')
|
|
677
|
+
: 'ninguna',
|
|
678
|
+
'grey',
|
|
679
|
+
),
|
|
680
|
+
);
|
|
681
|
+
perBout.forEach(({ bout, pending }) => boutCard(bout, pending));
|
|
682
|
+
const accept = recommended();
|
|
683
|
+
put(actions,
|
|
684
|
+
accept.length
|
|
685
|
+
? el('button', {
|
|
686
|
+
type: 'button',
|
|
687
|
+
class: 'now-accept',
|
|
688
|
+
text: `✓ Aceptar ${accept.length === 1 ? 'la recomendación' : `las ${accept.length} recomendaciones`} del réferi`,
|
|
689
|
+
onclick: confirmAccept,
|
|
690
|
+
})
|
|
691
|
+
: null,
|
|
692
|
+
open.length
|
|
693
|
+
? el('button', {
|
|
694
|
+
type: 'button',
|
|
695
|
+
class: 'now-one',
|
|
696
|
+
text: 'Revisar una por una',
|
|
697
|
+
onclick: () => {
|
|
698
|
+
const first = panel.querySelector('.now-q input[type="radio"]');
|
|
699
|
+
first
|
|
700
|
+
?.closest('.now-q')
|
|
701
|
+
.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
702
|
+
first?.focus();
|
|
703
|
+
},
|
|
704
|
+
})
|
|
705
|
+
: null,
|
|
706
|
+
open.length
|
|
707
|
+
? el('span', { text: 'cada respuesta vuelve al réferi como rebote' })
|
|
708
|
+
: null,
|
|
709
|
+
);
|
|
710
|
+
actions.hidden = !actions.childElementCount;
|
|
711
|
+
}
|
|
712
|
+
const schedule = () => {
|
|
713
|
+
if (queued) return;
|
|
714
|
+
queued = true;
|
|
715
|
+
setTimeout(paint, 0);
|
|
716
|
+
};
|
|
717
|
+
document.addEventListener('ring:change', schedule);
|
|
718
|
+
paint();
|
|
719
|
+
})();
|