Files
justice/www/html/Admin/admin.js
T

3026 lines
125 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(function () {
'use strict';
var API = '/Admin/api/';
/** ผ่าน PHP Admin เพื่อให้ PUT ถึง Node แม้ nginx ไม่ proxy /Game/api/ ไปยัง Node */
var GAME_QUIZ_API = '/Admin/api/game-quiz-settings.php';
var GAME_TIMING_API = '/Game/api/game-timing';
var GAME_GAUNTLET_ASSETS_API = '/Game/api/gauntlet-assets';
/** ลำดับอัปโหลดจริงอยู่ที่ quizCarryPlaqueUploadWithFallback — Node ก่อน แล้วค่อย PHP */
var GAME_QUIZ_CARRY_PLAQUE_UPLOAD_NODE = '/Game/api/quiz-carry-plaque-upload';
var GAME_QUIZ_CARRY_PLAQUE_UPLOAD_PHP = '/Admin/api/game-quiz-carry-plaque-upload.php';
/** >0 ระหว่างอัปโหลดรูปป้าย — ห้ามบันทึกก่อนจบไม่งั้น plaqueImageUrl ว่างถูกเขียนลง JSON */
var quizCarryPlaqueUploadPending = 0;
/** epoch ms เมื่อเริ่มอัปโหลดล่าสุด — กันสถานะค้างบล็อกบันทึกตลอด */
var quizCarryPlaqueUploadStartMs = 0;
var quizCarrySaveInFlight = false;
/** ซิงก์ range ↔ ตัวเลข carryChoicePlaqueMapScale (wire ครั้งเดียว) */
var quizCarryPlaqueScaleControlsWired = false;
/** สำรอง URL หลังอัปโหลดสำเร็จต่อช่อง — กัน race ที่ช่อง text ว่างตอน buildForSave แต่รูปอัปโหลดแล้วจริง */
var quizCarryPlaquePendingUrlBySlot = {};
var stackGameSaveInFlight = false;
/** โหมดแทนที่รูป: ชื่อไฟล์เป้าหมายก่อนเปิด file picker */
var gauntletReplaceTarget = null;
var cred = { credentials: 'include' };
function el(id) { return document.getElementById(id); }
function api(path, opts) {
opts = opts || {};
opts.credentials = 'include';
if (!opts.headers) opts.headers = {};
if (opts.body && typeof opts.body === 'object' && !(opts.body instanceof FormData)) {
opts.headers['Content-Type'] = 'application/json';
opts.body = JSON.stringify(opts.body);
}
return fetch(API + path, opts).then(function (r) {
return r.text().then(function (text) {
var j = {};
try {
j = text ? JSON.parse(text) : {};
} catch (e) {
if (!r.ok) throw new Error(text || r.statusText);
throw e;
}
if (!r.ok) throw new Error(j.error || r.statusText || 'Request failed');
return j;
});
});
}
function setMsg(id, text, cls) {
var n = el(id);
if (!n) return;
n.textContent = text || '';
n.className = 'msg' + (cls ? ' ' + cls : '');
}
function gameQuizFetch(method, body) {
var m = method || 'GET';
var url = GAME_QUIZ_API;
if (m === 'GET' && body == null) {
url += (GAME_QUIZ_API.indexOf('?') >= 0 ? '&' : '?') + '_=' + Date.now();
}
var opts = { method: m, credentials: 'include', cache: 'no-store' };
if (body != null) {
opts.headers = { 'Content-Type': 'application/json' };
opts.body = JSON.stringify(body);
}
return fetch(url, opts).then(function (r) {
return r.text().then(function (text) {
var j = {};
try {
j = text ? JSON.parse(text) : {};
} catch (e) {
if (!r.ok) {
throw new Error((text || '').slice(0, 280) || r.statusText || 'Request failed');
}
throw new Error('คำตอบไม่ใช่ JSON (HTTP ' + r.status + ') — ตรวจ nginx / PHP proxy');
}
if (!r.ok) {
throw new Error(j.error || ('HTTP ' + r.status + ' ' + r.statusText));
}
return j;
});
});
}
function gameTimingFetch(method, body) {
var m = method || 'GET';
var url = GAME_TIMING_API;
if (m === 'GET' && body == null) {
url += (GAME_TIMING_API.indexOf('?') >= 0 ? '&' : '?') + '_=' + Date.now();
}
var opts = { method: m, credentials: 'include', cache: 'no-store' };
if (body != null) {
opts.headers = { 'Content-Type': 'application/json' };
opts.body = JSON.stringify(body);
}
return fetch(url, opts).then(function (r) {
return r.text().then(function (text) {
var j = {};
try {
j = text ? JSON.parse(text) : {};
} catch (e) {
if (!r.ok) {
var plain = (text || r.statusText || 'Request failed').trim();
if (r.status === 404) {
plain += ' — รีสตาร์ท Node ที่รัน Game/server.js (pm2 / systemd) หลัง deploy · Restart Game Node after deploy';
}
throw new Error(plain);
}
throw e;
}
if (!r.ok) {
var base = j.error || r.statusText || 'Request failed';
if (r.status === 404) {
base += ' — รีสตาร์ท Node ที่รัน Game/server.js (pm2 / systemd) หลัง deploy · Restart Game Node after deploy';
}
throw new Error(base);
}
return j;
});
});
}
function addQuizAdminRow(text, answerTrue) {
var list = el('quiz-admin-questions-list');
if (!list) return;
var row = document.createElement('div');
row.className = 'quiz-admin-q-row';
var lab1 = document.createElement('label');
lab1.className = 'quiz-admin-q-label';
lab1.appendChild(document.createTextNode('คำถาม '));
var inp = document.createElement('input');
inp.type = 'text';
inp.className = 'quiz-admin-q-text';
inp.placeholder = 'พิมพ์คำถาม...';
inp.maxLength = 500;
inp.value = text || '';
lab1.appendChild(inp);
var lab2 = document.createElement('label');
lab2.className = 'quiz-admin-ans-label';
lab2.appendChild(document.createTextNode('คำตอบถูก '));
var sel = document.createElement('select');
sel.className = 'quiz-admin-q-ans';
[['true', 'จริง (ถูก)'], ['false', 'เท็จ (ผิด)']].forEach(function (pair) {
var o = document.createElement('option');
o.value = pair[0];
o.textContent = pair[1];
var isTrue = answerTrue !== false;
if (pair[0] === 'true' ? isTrue : !isTrue) o.selected = true;
sel.appendChild(o);
});
lab2.appendChild(sel);
var rm = document.createElement('button');
rm.type = 'button';
rm.className = 'btn btn-danger btn-sm quiz-admin-q-remove';
rm.setAttribute('aria-label', 'ลบข้อนี้');
rm.textContent = 'ลบ';
rm.addEventListener('click', function () {
row.remove();
if (!list.querySelector('.quiz-admin-q-row')) addQuizAdminRow('', true);
});
row.appendChild(lab1);
row.appendChild(lab2);
row.appendChild(rm);
list.appendChild(row);
}
function renderQuizAdminQuestions(arr) {
var list = el('quiz-admin-questions-list');
if (!list) return;
list.innerHTML = '';
var rows = (arr && arr.length) ? arr : [{ text: '', answerTrue: true }];
rows.forEach(function (q) {
addQuizAdminRow(q.text || '', q.answerTrue !== false);
});
}
function loadQuizSettingsPanel() {
gameQuizFetch('GET').then(function (data) {
el('quiz-read-sec').value = String(Math.round((data.readMs || 10000) / 1000));
el('quiz-answer-sec').value = String(Math.round((data.answerMs || 5000) / 1000));
var bMs = data.betweenMs != null ? data.betweenMs : 3500;
el('quiz-between-sec').value = String(Math.max(0, Math.round(bMs / 1000)));
renderQuizAdminQuestions(data.questions);
setMsg('quiz-settings-msg', '', '');
}).catch(function (e) {
setMsg('quiz-settings-msg', e.message || 'โหลดไม่ได้', 'error');
});
}
function saveQuizSettingsPanel() {
var readS = parseInt(el('quiz-read-sec').value, 10);
var ansS = parseInt(el('quiz-answer-sec').value, 10);
var betS = parseInt(el('quiz-between-sec').value, 10);
if (Number.isNaN(readS) || readS < 1) readS = 10;
if (Number.isNaN(ansS) || ansS < 1) ansS = 5;
if (Number.isNaN(betS) || betS < 0) betS = 3;
readS = Math.min(300, readS);
ansS = Math.min(300, ansS);
betS = Math.min(120, betS);
var questions = [];
document.querySelectorAll('#quiz-admin-questions-list .quiz-admin-q-row').forEach(function (row) {
var inp = row.querySelector('.quiz-admin-q-text');
var sel = row.querySelector('.quiz-admin-q-ans');
var t = inp && inp.value ? String(inp.value).trim() : '';
if (!t) return;
questions.push({ text: t, answerTrue: !!(sel && sel.value === 'true') });
});
gameQuizFetch('PUT', {
readMs: readS * 1000,
answerMs: ansS * 1000,
betweenMs: betS * 1000,
questions: questions,
}).then(function () {
setMsg('quiz-settings-msg', 'บันทึกแล้ว', 'ok');
}).catch(function (e) {
setMsg('quiz-settings-msg', e.message || 'บันทึกไม่ได้', 'error');
});
}
var QUIZ_CARRY_MAX_SLOTS = 16;
function rebuildQuizCarryCorrectSelect(row) {
var sel = row.querySelector('.quiz-carry-correct-sel');
if (!sel) return;
var inputs = row.querySelectorAll('.quiz-carry-slot-inp');
var prev = sel.value;
sel.innerHTML = '';
var has = false;
for (var i = 0; i < inputs.length; i++) {
var t = inputs[i].value ? String(inputs[i].value).trim() : '';
if (!t) continue;
has = true;
var o = document.createElement('option');
o.value = String(i);
var preview = t.length > 48 ? t.slice(0, 45) + '…' : t;
o.textContent = String(i + 1) + '. ' + preview;
sel.appendChild(o);
}
if (!has) {
var ox = document.createElement('option');
ox.value = '';
ox.textContent = '— กรอกตัวเลือกอย่างน้อย 2 ช่อง —';
sel.appendChild(ox);
sel.disabled = true;
return;
}
sel.disabled = false;
var ok = false;
for (var j = 0; j < sel.options.length; j++) {
if (sel.options[j].value === prev) {
sel.selectedIndex = j;
ok = true;
break;
}
}
if (!ok) sel.selectedIndex = 0;
}
function bindQuizCarryRowInputs(row) {
row.querySelectorAll('.quiz-carry-slot-inp').forEach(function (inp) {
inp.addEventListener('input', function () {
rebuildQuizCarryCorrectSelect(row);
});
});
}
function addQuizCarryAdminRow(q) {
var list = el('quiz-carry-admin-list');
if (!list) return;
q = q || {};
var choices = Array.isArray(q.choices) ? q.choices : [];
var choiceImages = Array.isArray(q.choiceImageUrls) ? q.choiceImageUrls : [];
var block = document.createElement('div');
block.className = 'quiz-carry-admin-block';
var head = document.createElement('div');
head.className = 'quiz-carry-admin-block-head';
var labQ = document.createElement('label');
labQ.className = 'quiz-carry-q-label';
labQ.appendChild(document.createTextNode('คำถาม'));
var inpQ = document.createElement('input');
inpQ.type = 'text';
inpQ.className = 'quiz-carry-q-text';
inpQ.placeholder = 'พิมพ์คำถาม…';
inpQ.maxLength = 500;
inpQ.value = q.text ? String(q.text) : '';
labQ.appendChild(inpQ);
var rm = document.createElement('button');
rm.type = 'button';
rm.className = 'btn btn-danger btn-sm quiz-carry-block-remove';
rm.setAttribute('aria-label', 'ลบข้อนี้');
rm.textContent = 'ลบข้อ';
rm.addEventListener('click', function () {
block.remove();
if (!list.querySelector('.quiz-carry-admin-block')) addQuizCarryAdminRow(null);
});
head.appendChild(labQ);
head.appendChild(rm);
block.appendChild(head);
var grid = document.createElement('div');
grid.className = 'quiz-carry-choice-grid';
for (var s = 0; s < QUIZ_CARRY_MAX_SLOTS; s++) {
var lab = document.createElement('label');
lab.className = 'quiz-carry-slot-label';
lab.appendChild(document.createTextNode('ตัวเลือก ' + (s + 1)));
var inp = document.createElement('input');
inp.type = 'text';
inp.className = 'quiz-carry-slot-inp';
inp.placeholder = s < 2 ? 'จำเป็น' : 'ไม่บังคับ';
inp.maxLength = 160;
inp.value = choices[s] != null ? String(choices[s]) : '';
lab.appendChild(inp);
var imgInp = document.createElement('input');
imgInp.type = 'url';
imgInp.className = 'quiz-carry-slot-img';
imgInp.placeholder = 'รูป (URL — ไม่บังคับ)';
imgInp.maxLength = 512;
imgInp.autocomplete = 'off';
imgInp.value = choiceImages[s] != null ? String(choiceImages[s]) : '';
imgInp.setAttribute('aria-label', 'URL รูปตัวเลือก ' + (s + 1));
lab.appendChild(imgInp);
grid.appendChild(lab);
}
block.appendChild(grid);
var cw = document.createElement('div');
cw.className = 'quiz-carry-correct-wrap';
var labC = document.createElement('label');
labC.className = 'quiz-carry-correct-label';
labC.appendChild(document.createTextNode('ข้อที่ถูกต้อง'));
var sel = document.createElement('select');
sel.className = 'quiz-carry-correct-sel';
sel.setAttribute('aria-label', 'เลือกข้อที่ถูก');
labC.appendChild(sel);
cw.appendChild(labC);
block.appendChild(cw);
list.appendChild(block);
bindQuizCarryRowInputs(block);
rebuildQuizCarryCorrectSelect(block);
var ci = Number(q.correctIndex);
if (Number.isFinite(ci) && ci >= 0) {
var targetSlot = ci;
var opts = sel.querySelectorAll('option');
for (var k = 0; k < opts.length; k++) {
if (opts[k].value === String(targetSlot)) {
sel.selectedIndex = k;
break;
}
}
}
}
function renderQuizCarryAdminList(arr) {
var list = el('quiz-carry-admin-list');
if (!list) return;
list.innerHTML = '';
var rows = arr && arr.length ? arr : [null];
rows.forEach(function (q) {
addQuizCarryAdminRow(q);
});
}
/** ค่าเริ่มต้น RGBA สำหรับแผงคำถามบนแผนที่ (quiz_carry) */
var QUIZ_CARRY_THEME_DEFAULTS = {
bg: { r: 12, g: 14, b: 28, a: 0.88 },
border: { r: 255, g: 214, b: 102, a: 0.7 },
text: { r: 241, g: 245, b: 249, a: 1 },
};
function quizCarryClampByte(n) {
return Math.max(0, Math.min(255, Math.round(Number(n)) || 0));
}
function quizCarryHexFromRgb(r, g, b) {
return '#' + [r, g, b].map(function (x) {
var h = quizCarryClampByte(x).toString(16);
return h.length === 1 ? '0' + h : h;
}).join('');
}
function quizCarryParseHexToRgb(hex) {
var h = String(hex || '').trim().replace(/^#/, '');
if (!h) return null;
if (h.length === 3) {
return {
r: parseInt(h[0] + h[0], 16),
g: parseInt(h[1] + h[1], 16),
b: parseInt(h[2] + h[2], 16),
alpha01: null,
};
}
if (h.length === 6) {
return {
r: parseInt(h.slice(0, 2), 16),
g: parseInt(h.slice(2, 4), 16),
b: parseInt(h.slice(4, 6), 16),
alpha01: null,
};
}
if (h.length === 8) {
return {
r: parseInt(h.slice(0, 2), 16),
g: parseInt(h.slice(2, 4), 16),
b: parseInt(h.slice(4, 6), 16),
alpha01: parseInt(h.slice(6, 8), 16) / 255,
};
}
return null;
}
function quizCarryParseCssColor(str, fallback) {
var s = String(str == null ? '' : str).trim();
if (!s) return fallback;
var m = /^rgba?\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*(?:,\s*([0-9.]+)\s*)?\)$/i.exec(s);
if (m) {
var a = m[4] != null && m[4] !== '' ? parseFloat(m[4]) : 1;
if (!Number.isFinite(a)) a = 1;
return {
r: quizCarryClampByte(parseInt(m[1], 10)),
g: quizCarryClampByte(parseInt(m[2], 10)),
b: quizCarryClampByte(parseInt(m[3], 10)),
a: Math.max(0, Math.min(1, a)),
};
}
if (/^#/.test(s)) {
var pr = quizCarryParseHexToRgb(s);
if (!pr) return fallback;
var a = pr.alpha01 != null ? pr.alpha01 : 1;
return { r: pr.r, g: pr.g, b: pr.b, a: Math.max(0, Math.min(1, a)) };
}
return fallback;
}
function quizCarryRgbToRgbaString(o) {
var a = Math.max(0, Math.min(1, Number(o.a)));
var t = Math.round(a * 1000) / 1000;
return 'rgba(' + quizCarryClampByte(o.r) + ', ' + quizCarryClampByte(o.g) + ', ' + quizCarryClampByte(o.b) + ', ' + t + ')';
}
function quizCarryRowPrefix(kind) {
return 'quiz-carry-theme-' + kind;
}
function quizCarryThemeHiddenId(kind) {
if (kind === 'bg') return 'quiz-carry-theme-bg';
if (kind === 'border') return 'quiz-carry-theme-border';
return 'quiz-carry-theme-text';
}
function quizCarrySetPickersFromColorString(kind, cssStr) {
var fb = QUIZ_CARRY_THEME_DEFAULTS[kind];
var o = quizCarryParseCssColor(cssStr, fb);
var pref = quizCarryRowPrefix(kind);
var sw = el(pref + '-swatch');
var ra = el(pref + '-alpha');
var out = el(pref + '-alpha-out');
var hi = el(quizCarryThemeHiddenId(kind));
var pr = el(pref + '-preview');
if (!sw || !ra || !hi) return;
sw.value = quizCarryHexFromRgb(o.r, o.g, o.b);
var pct = Math.max(0, Math.min(100, Math.round(Number(o.a) * 100)));
ra.value = String(pct);
if (out) out.textContent = pct + '%';
hi.value = quizCarryRgbToRgbaString(o);
if (pr) pr.textContent = hi.value;
}
function quizCarryComposeFromPickers(kind) {
var pref = quizCarryRowPrefix(kind);
var sw = el(pref + '-swatch');
var ra = el(pref + '-alpha');
if (!sw || !ra) return '';
var pr = quizCarryParseHexToRgb(sw.value);
if (!pr) return '';
var pct = parseInt(String(ra.value), 10);
if (!Number.isFinite(pct)) pct = 100;
pct = Math.max(0, Math.min(100, pct));
return quizCarryRgbToRgbaString({ r: pr.r, g: pr.g, b: pr.b, a: pct / 100 });
}
function quizCarrySyncRowOutput(kind) {
var s = quizCarryComposeFromPickers(kind);
var hi = el(quizCarryThemeHiddenId(kind));
var pref = quizCarryRowPrefix(kind);
var ra = el(pref + '-alpha');
var out = el(pref + '-alpha-out');
var pr = el(pref + '-preview');
if (hi) hi.value = s;
if (out && ra) out.textContent = String(Math.max(0, Math.min(100, parseInt(ra.value, 10) || 0))) + '%';
if (pr && hi) pr.textContent = hi.value;
}
/** อ่านค่าจาก color + range โดยตรง (ไม่พึ่ง hidden ที่อาจว่าง) ก่อน PUT */
function quizCarryBuildThemeForSave() {
quizCarryBindThemePickersOnce();
['bg', 'border', 'text'].forEach(function (k) {
quizCarrySyncRowOutput(k);
});
var fbBg = QUIZ_CARRY_THEME_DEFAULTS.bg;
var fbBr = QUIZ_CARRY_THEME_DEFAULTS.border;
var fbTx = QUIZ_CARRY_THEME_DEFAULTS.text;
var sBg = quizCarryComposeFromPickers('bg');
var sBr = quizCarryComposeFromPickers('border');
var sTx = quizCarryComposeFromPickers('text');
var bwEl = el('quiz-carry-theme-border-w');
var bw = bwEl ? parseInt(String(bwEl.value || '2'), 10) : 2;
if (!Number.isFinite(bw)) bw = 2;
bw = Math.max(0, Math.min(12, Math.round(bw)));
var qfMinEl = el('quiz-carry-theme-qfont-min');
var qfMaxEl = el('quiz-carry-theme-qfont-max');
var qMin = qfMinEl ? parseInt(String(qfMinEl.value || '10'), 10) : 10;
var qMax = qfMaxEl ? parseInt(String(qfMaxEl.value || '24'), 10) : 24;
if (!Number.isFinite(qMin)) qMin = 10;
if (!Number.isFinite(qMax)) qMax = 24;
qMin = Math.max(10, Math.min(40, Math.round(qMin)));
qMax = Math.max(14, Math.min(56, Math.round(qMax)));
if (qMax < qMin) {
var swap = qMin;
qMin = qMax;
qMax = swap;
}
return {
panelBg: (sBg && sBg.trim()) ? sBg.trim().slice(0, 120) : quizCarryRgbToRgbaString(fbBg),
panelBorder: (sBr && sBr.trim()) ? sBr.trim().slice(0, 120) : quizCarryRgbToRgbaString(fbBr),
textColor: (sTx && sTx.trim()) ? sTx.trim().slice(0, 120) : quizCarryRgbToRgbaString(fbTx),
borderWidthPx: bw,
questionFontMinPx: qMin,
questionFontMaxPx: qMax,
};
}
function quizCarryBindThemePickersOnce() {
if (quizCarryBindThemePickersOnce._done) return;
['bg', 'border', 'text'].forEach(function (kind) {
var pref = quizCarryRowPrefix(kind);
var sw = el(pref + '-swatch');
var ra = el(pref + '-alpha');
if (!sw || !ra) return;
if (ra.getAttribute('data-bound') === '1') return;
ra.setAttribute('data-bound', '1');
function sync() {
quizCarrySyncRowOutput(kind);
}
sw.addEventListener('input', sync);
sw.addEventListener('change', sync);
ra.addEventListener('input', sync);
ra.addEventListener('change', sync);
});
quizCarryBindThemePickersOnce._done = true;
}
/** ธีมนับ 3-2-1 embed — สีม่าน / กล่อง / ขนาด (carryEmbedCountdownTheme) */
var QUIZ_CARRY_ECD_COLS = [
{ sw: 'quiz-carry-ecd-overlay-swatch', ra: 'quiz-carry-ecd-overlay-alpha', out: 'quiz-carry-ecd-overlay-alpha-out', hi: 'quiz-carry-ecd-overlay-val', pr: 'quiz-carry-ecd-overlay-preview' },
{ sw: 'quiz-carry-ecd-inner-bg-swatch', ra: 'quiz-carry-ecd-inner-bg-alpha', out: 'quiz-carry-ecd-inner-bg-alpha-out', hi: 'quiz-carry-ecd-inner-bg-val', pr: 'quiz-carry-ecd-inner-bg-preview' },
{ sw: 'quiz-carry-ecd-inner-border-swatch', ra: 'quiz-carry-ecd-inner-border-alpha', out: 'quiz-carry-ecd-inner-border-alpha-out', hi: 'quiz-carry-ecd-inner-border-val', pr: 'quiz-carry-ecd-inner-border-preview' },
];
var QUIZ_CARRY_ECD_DEFAULTS_RGBA = {
overlay: { r: 8, g: 10, b: 20, a: 0.42 },
innerBg: { r: 12, g: 14, b: 28, a: 0.82 },
innerBorder: { r: 122, g: 162, b: 247, a: 0.45 },
};
function quizCarryEcdColKey(idx) {
if (idx === 0) return 'overlay';
if (idx === 1) return 'innerBg';
return 'innerBorder';
}
function quizCarryEcdSyncRow(idx) {
var row = QUIZ_CARRY_ECD_COLS[idx];
var sw = el(row.sw);
var ra = el(row.ra);
var hi = el(row.hi);
var pr = el(row.pr);
var out = el(row.out);
if (!sw || !ra || !hi) return;
var prRgb = quizCarryParseHexToRgb(sw.value);
if (!prRgb) return;
var pct = parseInt(String(ra.value), 10);
if (!Number.isFinite(pct)) pct = 100;
pct = Math.max(0, Math.min(100, pct));
var s = quizCarryRgbToRgbaString({ r: prRgb.r, g: prRgb.g, b: prRgb.b, a: pct / 100 });
hi.value = s;
if (out) out.textContent = pct + '%';
if (pr) pr.textContent = s;
}
function quizCarryEcdSetRowFromRgba(idx, cssStr) {
var row = QUIZ_CARRY_ECD_COLS[idx];
var key = quizCarryEcdColKey(idx);
var fb = QUIZ_CARRY_ECD_DEFAULTS_RGBA[key];
var o = quizCarryParseCssColor(cssStr, fb);
var sw = el(row.sw);
var ra = el(row.ra);
var out = el(row.out);
var hi = el(row.hi);
var pr = el(row.pr);
if (!sw || !ra || !hi) return;
sw.value = quizCarryHexFromRgb(o.r, o.g, o.b);
var pct = Math.max(0, Math.min(100, Math.round(Number(o.a) * 100)));
ra.value = String(pct);
if (out) out.textContent = pct + '%';
hi.value = quizCarryRgbToRgbaString(o);
if (pr) pr.textContent = hi.value;
}
function quizCarryEcdBindPickersOnce() {
if (quizCarryEcdBindPickersOnce._done) return;
for (var i = 0; i < QUIZ_CARRY_ECD_COLS.length; i++) {
(function (idx) {
var row = QUIZ_CARRY_ECD_COLS[idx];
var sw = el(row.sw);
var ra = el(row.ra);
if (!sw || !ra) return;
if (ra.getAttribute('data-ecd-bound') === '1') return;
ra.setAttribute('data-ecd-bound', '1');
function sync() {
quizCarryEcdSyncRow(idx);
}
sw.addEventListener('input', sync);
sw.addEventListener('change', sync);
ra.addEventListener('input', sync);
ra.addEventListener('change', sync);
})(i);
}
quizCarryEcdBindPickersOnce._done = true;
}
function quizCarryEcdBuildForSave() {
quizCarryEcdBindPickersOnce();
for (var i = 0; i < QUIZ_CARRY_ECD_COLS.length; i++) quizCarryEcdSyncRow(i);
var o0 = el('quiz-carry-ecd-overlay-val');
var o1 = el('quiz-carry-ecd-inner-bg-val');
var o2 = el('quiz-carry-ecd-inner-border-val');
var bwEl = el('quiz-carry-ecd-inner-border-w');
var radEl = el('quiz-carry-ecd-inner-radius');
var dig = el('quiz-carry-ecd-digit-swatch');
var m1 = el('quiz-carry-ecd-map-cqmin');
var m2 = el('quiz-carry-ecd-map-cqh');
var m3 = el('quiz-carry-ecd-map-max-px');
var sv = el('quiz-carry-ecd-screen-vw');
var sm = el('quiz-carry-ecd-screen-max-px');
var bw = bwEl ? parseInt(String(bwEl.value || '1'), 10) : 1;
if (!Number.isFinite(bw)) bw = 1;
bw = Math.max(0, Math.min(12, Math.round(bw)));
var rad = radEl ? parseInt(String(radEl.value || '12'), 10) : 12;
if (!Number.isFinite(rad)) rad = 12;
rad = Math.max(0, Math.min(32, Math.round(rad)));
var cqmin = m1 ? parseInt(String(m1.value || '78'), 10) : 78;
var cqh = m2 ? parseInt(String(m2.value || '82'), 10) : 82;
var mpx = m3 ? parseInt(String(m3.value || '200'), 10) : 200;
cqmin = Math.max(35, Math.min(100, cqmin));
cqh = Math.max(35, Math.min(100, cqh));
mpx = Math.max(48, Math.min(400, mpx));
var vw = sv ? parseInt(String(sv.value || '28'), 10) : 28;
var smx = sm ? parseInt(String(sm.value || '132'), 10) : 132;
vw = Math.max(6, Math.min(44, vw));
smx = Math.max(48, Math.min(220, smx));
var digitHex = dig && /^#[0-9a-fA-F]{6}$/.test(String(dig.value || '').trim()) ? String(dig.value).trim() : '#ffe066';
return {
overlayBackdrop: (o0 && o0.value.trim()) ? o0.value.trim().slice(0, 120) : quizCarryRgbToRgbaString(QUIZ_CARRY_ECD_DEFAULTS_RGBA.overlay),
innerBg: (o1 && o1.value.trim()) ? o1.value.trim().slice(0, 120) : quizCarryRgbToRgbaString(QUIZ_CARRY_ECD_DEFAULTS_RGBA.innerBg),
innerBorder: (o2 && o2.value.trim()) ? o2.value.trim().slice(0, 120) : quizCarryRgbToRgbaString(QUIZ_CARRY_ECD_DEFAULTS_RGBA.innerBorder),
innerBorderWpx: bw,
innerRadiusPx: rad,
digitColor: digitHex.slice(0, 32),
mapDigitCqmin: cqmin,
mapDigitCqh: cqh,
mapDigitMaxPx: mpx,
screenDigitVw: vw,
screenDigitMaxPx: smx,
};
}
function quizCarryEcdFillForm(th) {
quizCarryEcdBindPickersOnce();
var t = th && typeof th === 'object' ? th : {};
quizCarryEcdSetRowFromRgba(0, t.overlayBackdrop);
quizCarryEcdSetRowFromRgba(1, t.innerBg);
quizCarryEcdSetRowFromRgba(2, t.innerBorder);
var bwEl = el('quiz-carry-ecd-inner-border-w');
var radEl = el('quiz-carry-ecd-inner-radius');
var dig = el('quiz-carry-ecd-digit-swatch');
var m1 = el('quiz-carry-ecd-map-cqmin');
var m2 = el('quiz-carry-ecd-map-cqh');
var m3 = el('quiz-carry-ecd-map-max-px');
var sv = el('quiz-carry-ecd-screen-vw');
var sm = el('quiz-carry-ecd-screen-max-px');
var bw = parseInt(String(t.innerBorderWpx), 10);
if (bwEl) bwEl.value = String(Number.isFinite(bw) && bw >= 0 ? Math.min(12, bw) : 1);
var rad = parseInt(String(t.innerRadiusPx), 10);
if (radEl) radEl.value = String(Number.isFinite(rad) && rad >= 0 ? Math.min(32, rad) : 12);
if (dig && typeof t.digitColor === 'string' && /^#[0-9a-fA-F]{6}$/.test(t.digitColor.trim())) dig.value = t.digitColor.trim();
var cqmin = parseInt(String(t.mapDigitCqmin), 10);
var cqh = parseInt(String(t.mapDigitCqh), 10);
var mpx = parseInt(String(t.mapDigitMaxPx), 10);
if (m1) m1.value = String(Number.isFinite(cqmin) ? Math.max(35, Math.min(100, cqmin)) : 78);
if (m2) m2.value = String(Number.isFinite(cqh) ? Math.max(35, Math.min(100, cqh)) : 82);
if (m3) m3.value = String(Number.isFinite(mpx) ? Math.max(48, Math.min(400, mpx)) : 200);
var vw = parseInt(String(t.screenDigitVw), 10);
var smx = parseInt(String(t.screenDigitMaxPx), 10);
if (sv) sv.value = String(Number.isFinite(vw) ? Math.max(6, Math.min(44, vw)) : 28);
if (sm) sm.value = String(Number.isFinite(smx) ? Math.max(48, Math.min(220, smx)) : 132);
}
var QUIZ_CARRY_PLAQUE_SLOT_COUNT = 16;
var QUIZ_CARRY_PLAQUE_KINDS = ['fixed', 'fill', 'text'];
var QUIZ_CARRY_PLAQUE_DEFAULTS_RGBA = {
fixed: { r: 122, g: 200, b: 255, a: 0.9 },
fill: { r: 12, g: 10, b: 20, a: 0.88 },
text: { r: 248, g: 249, b: 255, a: 1 },
};
function quizCarryPlaqueSlotPrefix(si) {
return 'quiz-carry-plaque-s' + si + '-';
}
/**
* อัปโหลดป้าย: ลอง POST ตรง Node (/Game/api/ — nginx proxy 20M) ก่อน แล้วค่อย PHP ถ้าล้ม
* เดิมลอง PHP ก่อนทำให้พังง่ายเมื่อ php-fpm ต่อ 127.0.0.1:3001 ไม่ได้หรือ session ผิด แม้เกมจะรันอยู่
*/
function quizCarryPlaqueUploadWithFallback(dataUrl) {
var body = JSON.stringify({ imageDataUrl: dataUrl });
var opts = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: body };
return gauntletAssetsJsonFetch(GAME_QUIZ_CARRY_PLAQUE_UPLOAD_NODE, opts).catch(function (errFirst) {
var msg = String((errFirst && errFirst.message) || '');
if (/\b401\b|Unauthorized|หมดเซสชัน/i.test(msg)) {
return Promise.reject(errFirst);
}
var tryPhp =
/\b404\b|\b405\b|Not Found|ไม่พบ|502|503|504|Bad Gateway|Failed to fetch|NetworkError|Load failed|ECONNREFUSED/i.test(
msg,
) ||
/\b413\b|Request Entity Too Large|Payload too large|รับ body ไม่ได้|too large/i.test(msg);
if (!tryPhp) {
return Promise.reject(errFirst);
}
return gauntletAssetsJsonFetch(GAME_QUIZ_CARRY_PLAQUE_UPLOAD_PHP, opts).then(function (res) {
try {
setMsg(
'quiz-carry-settings-msg',
'อัปโหลดสำเร็จผ่าน PHP (รองจาก /Game/api/) · Upload OK via PHP fallback',
'ok',
);
} catch (e1) { /* ignore */ }
return res;
});
});
}
/** URL สำหรับ <img src> — path ที่ขึ้นต้นด้วย / ให้ต่อ origin (รองรับ Admin ใต้โดเมนเดียวกับ /Game/) */
function quizCarryPlaqueAbsMediaUrl(path) {
var s = String(path || '').trim();
if (!s) return '';
if (/^https?:\/\//i.test(s)) return s;
if (s.charAt(0) === '/') return String(window.location.origin || '') + s;
return '';
}
function quizCarryPlaqueUpdateImagePreview(si) {
var pfx = quizCarryPlaqueSlotPrefix(si);
var inp = el(pfx + 'plaque-image-url');
var img = el(pfx + 'plaque-preview-img');
var errEl = el(pfx + 'plaque-preview-err');
if (!img) return;
var raw = inp && inp.value ? String(inp.value).trim() : '';
if (errEl) errEl.textContent = '';
if (!raw) {
img.style.display = 'none';
img.removeAttribute('src');
return;
}
var abs = quizCarryPlaqueAbsMediaUrl(raw);
if (!abs) {
img.style.display = 'none';
if (errEl) errEl.textContent = 'รูปแบบ URL ไม่รองรับ';
return;
}
img.onerror = function () {
img.style.display = 'none';
if (errEl) errEl.textContent = 'โหลดตัวอย่างไม่ได้ — ตรวจ path / สิทธิ์ไฟล์ / รีสตาร์ท Node';
};
img.onload = function () {
try {
img.removeAttribute('width');
img.removeAttribute('height');
} catch (e) { /* ignore */ }
img.style.display = 'block';
if (errEl) errEl.textContent = '';
};
var sep = abs.indexOf('?') >= 0 ? '&' : '?';
img.src = abs + sep + '_pv=' + Date.now();
}
function quizCarryPlaqueUpdateNeonUi(si) {
var modeEl = el(quizCarryPlaqueSlotPrefix(si) + 'border-mode');
var grid = modeEl && modeEl.closest ? modeEl.closest('.quiz-carry-theme-grid') : null;
if (!grid) return;
if (modeEl && modeEl.value === 'fixed') grid.classList.remove('quiz-carry-plaque-grid--neon');
else grid.classList.add('quiz-carry-plaque-grid--neon');
var hint = grid.querySelector('.quiz-carry-plaque-neon-hint');
if (hint) hint.style.display = modeEl && modeEl.value === 'fixed' ? 'none' : '';
}
function quizCarryPlaqueRowIds(si, kind) {
var seg = kind === 'fixed' ? 'fixed' : kind === 'fill' ? 'fill' : 'text';
var p = quizCarryPlaqueSlotPrefix(si);
return {
sw: p + seg + '-swatch',
ra: p + seg + '-alpha',
out: p + seg + '-alpha-out',
hi: p + seg + '-val',
};
}
function quizCarryPlaqueEnsureMounted() {
var root = el('quiz-carry-plaque-slots-root');
if (!root || root.getAttribute('data-mounted') === '1') return;
for (var si = 0; si < QUIZ_CARRY_PLAQUE_SLOT_COUNT; si++) {
var det = document.createElement('details');
det.className = 'quiz-carry-plaque-slot-details';
if (si === 0) det.open = true;
var summ = document.createElement('summary');
summ.textContent = 'ตัวเลือก ' + (si + 1);
det.appendChild(summ);
var grid = document.createElement('div');
grid.className = 'quiz-carry-theme-grid quiz-carry-plaque-grid';
var rowMode = document.createElement('div');
rowMode.className = 'quiz-carry-theme-row quiz-carry-theme-row--narrow';
rowMode.style.marginBottom = '0.45rem';
var labMode = document.createElement('label');
labMode.className = 'admin-field quiz-carry-theme-label';
labMode.appendChild(document.createTextNode('โหมดขอบป้าย'));
var selMode = document.createElement('select');
selMode.id = quizCarryPlaqueSlotPrefix(si) + 'border-mode';
selMode.setAttribute('aria-label', 'โหมดขอบป้ายช่อง ' + (si + 1));
[['neon', 'เรืองตามช่อง (neon)'], ['fixed', 'สีขอบคงที่']].forEach(function (opt) {
var o = document.createElement('option');
o.value = opt[0];
o.textContent = opt[1];
selMode.appendChild(o);
});
labMode.appendChild(selMode);
rowMode.appendChild(labMode);
grid.appendChild(rowMode);
selMode.addEventListener('change', function () {
quizCarryPlaqueUpdateNeonUi(si);
});
var hintNeon = document.createElement('p');
hintNeon.className = 'muted quiz-carry-plaque-neon-hint';
hintNeon.style.cssText = 'margin:0 0 0.5rem;font-size:0.78rem;line-height:1.45';
hintNeon.innerHTML =
'<strong>Neon / เรืองตามช่อง:</strong> ขอบบนแมปใช้สีตามเลขช่อง ไม่ใช้ «สีขอบ (fixed)» ด้านล่าง — ต้องการขอบตาม picker ให้เลือก <strong>สีขอบคงที่</strong> · ' +
'<em>English:</em> In <strong>Neon</strong> mode the map uses per-slot hue; <strong>Fixed border</strong> uses the fixed-border pickers.';
grid.appendChild(hintNeon);
function addColorRow(labelText, kind, defHex, defAlpha) {
var ids = quizCarryPlaqueRowIds(si, kind);
var row = document.createElement('div');
row.className = 'quiz-carry-theme-row';
row.setAttribute('data-quiz-carry-plaque-color-row', kind);
var lbl = document.createElement('span');
lbl.className = 'quiz-carry-theme-label';
lbl.textContent = labelText;
row.appendChild(lbl);
var controls = document.createElement('div');
controls.className = 'quiz-carry-color-controls';
var sw = document.createElement('input');
sw.type = 'color';
sw.id = ids.sw;
sw.value = defHex;
sw.title = labelText;
controls.appendChild(sw);
var labA = document.createElement('label');
labA.className = 'quiz-carry-alpha-label';
var spanA = document.createElement('span');
spanA.textContent = 'โปร่ง A';
labA.appendChild(spanA);
var ra = document.createElement('input');
ra.type = 'range';
ra.id = ids.ra;
ra.min = '0';
ra.max = '100';
ra.step = '1';
ra.value = String(defAlpha);
labA.appendChild(ra);
var out = document.createElement('output');
out.id = ids.out;
out.className = 'quiz-carry-alpha-out';
out.setAttribute('for', ids.ra);
out.textContent = defAlpha + '%';
labA.appendChild(out);
controls.appendChild(labA);
var hi = document.createElement('input');
hi.type = 'hidden';
hi.id = ids.hi;
controls.appendChild(hi);
row.appendChild(controls);
grid.appendChild(row);
}
addColorRow('สีขอบ (fixed)', 'fixed', '#7ac8ff', 90);
addColorRow('พื้นหลังกล่อง', 'fill', '#0c0a14', 88);
addColorRow('สีตัวอักษร', 'text', '#f8f9ff', 100);
var rowW = document.createElement('div');
rowW.className = 'quiz-carry-theme-row quiz-carry-theme-row--narrow';
var labW = document.createElement('label');
labW.className = 'admin-field quiz-carry-theme-label';
labW.appendChild(document.createTextNode('ความหนาขอบ (px)'));
var inpW = document.createElement('input');
inpW.type = 'number';
inpW.className = 'admin-inp-num';
inpW.id = quizCarryPlaqueSlotPrefix(si) + 'border-w';
inpW.min = '0';
inpW.max = '8';
inpW.step = '0.5';
inpW.value = '2.5';
labW.appendChild(inpW);
rowW.appendChild(labW);
grid.appendChild(rowW);
var rowImg = document.createElement('div');
/* ห้ามใช้ quiz-carry-theme-row--narrow — มี max-width:12rem ทำช่อง URL แคบมาก ดูเหมือนอัปโหลดไม่ทำงาน */
rowImg.className = 'quiz-carry-theme-row quiz-carry-plaque-image-row';
rowImg.style.cssText = 'flex-wrap:wrap;gap:0.65rem;align-items:flex-end';
var labUrl = document.createElement('label');
labUrl.className = 'admin-field';
labUrl.style.cssText = 'flex:1;min-width:200px;margin:0';
labUrl.appendChild(document.createTextNode('รูปบนป้าย (ช่องนี้)'));
var inpImgUrl = document.createElement('input');
inpImgUrl.type = 'text';
inpImgUrl.className = 'admin-inp-text';
inpImgUrl.id = quizCarryPlaqueSlotPrefix(si) + 'plaque-image-url';
inpImgUrl.placeholder = '/Game/img/quiz-carry-plaque-assets/... หรือ https://...';
inpImgUrl.setAttribute('aria-label', 'URL รูปป้ายช่อง ' + (si + 1));
labUrl.appendChild(document.createElement('br'));
labUrl.appendChild(inpImgUrl);
var prevErr = document.createElement('span');
prevErr.id = quizCarryPlaqueSlotPrefix(si) + 'plaque-preview-err';
prevErr.className = 'muted quiz-carry-plaque-preview-err';
prevErr.style.cssText = 'display:block;font-size:0.72rem;margin:0.25rem 0 0.15rem;min-height:1em';
labUrl.appendChild(prevErr);
var prevImg = document.createElement('img');
prevImg.id = quizCarryPlaqueSlotPrefix(si) + 'plaque-preview-img';
prevImg.alt = 'พรีวิวรูปป้ายช่อง ' + (si + 1);
prevImg.className = 'quiz-carry-plaque-preview-img';
/* ห้าม width/height = 0 — บางเบราว์เซอร์แสดงกล่อง 0×0 แม้ onload แล้ว · Don't use 0×0 attrs or preview stays invisible */
prevImg.style.cssText = 'display:none;max-height:120px;max-width:min(100%,320px);width:auto;height:auto;border-radius:8px;border:1px solid var(--border);object-fit:contain;background:var(--card)';
labUrl.appendChild(prevImg);
inpImgUrl.addEventListener('input', function () {
var v = String(inpImgUrl.value || '').trim();
if (!v) delete quizCarryPlaquePendingUrlBySlot[si];
quizCarryPlaqueUpdateImagePreview(si);
});
inpImgUrl.addEventListener('change', function () {
var v2 = String(inpImgUrl.value || '').trim();
if (!v2) delete quizCarryPlaquePendingUrlBySlot[si];
quizCarryPlaqueUpdateImagePreview(si);
});
rowImg.appendChild(labUrl);
var upWrap = document.createElement('div');
upWrap.className = 'admin-field';
upWrap.style.margin = '0';
var fileInp = document.createElement('input');
fileInp.type = 'file';
fileInp.accept = 'image/png,image/jpeg,image/webp,image/gif';
fileInp.id = quizCarryPlaqueSlotPrefix(si) + 'plaque-image-file';
fileInp.style.fontSize = '0.82rem';
var upMsg = document.createElement('span');
upMsg.className = 'muted';
upMsg.id = quizCarryPlaqueSlotPrefix(si) + 'plaque-image-msg';
upMsg.style.cssText = 'display:block;font-size:0.75rem;margin-top:0.25rem';
fileInp.addEventListener('change', function () {
while (upMsg.firstChild) upMsg.removeChild(upMsg.firstChild);
upMsg.textContent = '';
if (!fileInp.files || !fileInp.files[0]) return;
upMsg.textContent = 'กำลังอ่านไฟล์และอัปโหลด… · Reading file & uploading…';
try {
upMsg.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
} catch (scrollErr) { /* ignore */ }
quizCarryPlaqueUploadPending++;
quizCarryPlaqueUploadStartMs = Date.now();
readFileAsDataURL(fileInp.files[0])
.then(function (dataUrl) {
return quizCarryPlaqueUploadWithFallback(dataUrl);
})
.then(function (res) {
if (res && res.url) {
var uOk = String(res.url).trim();
inpImgUrl.value = uOk;
quizCarryPlaquePendingUrlBySlot[si] = uOk;
quizCarryPlaqueUpdateImagePreview(si);
upMsg.textContent = '';
var ok1 = document.createElement('span');
ok1.textContent = 'อัปโหลดแล้ว · path ที่ได้: ';
upMsg.appendChild(ok1);
var codeU = document.createElement('code');
codeU.style.cssText = 'font-size:0.78rem;word-break:break-all';
codeU.textContent = res.url;
upMsg.appendChild(codeU);
var br = document.createElement('br');
upMsg.appendChild(br);
var ok2 = document.createElement('span');
ok2.textContent = 'กด «บันทึก» ด้านล่างเพื่อเขียนลง quiz-settings.json · Then click Save below';
upMsg.appendChild(ok2);
} else {
upMsg.textContent = (res && res.error) || 'ตอบกลับไม่มี url — ตรวจ Node /Game/api/quiz-carry-plaque-upload';
}
})
.catch(function (e) {
upMsg.textContent = e.message || 'อัปโหลดไม่สำเร็จ';
setMsg('quiz-carry-settings-msg', e.message || 'อัปโหลดรูปป้ายไม่สำเร็จ · Plaque upload failed', 'error');
try {
upMsg.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
var smUp = el('quiz-carry-settings-msg');
if (smUp) smUp.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
} catch (scrollE) { /* ignore */ }
})
.finally(function () {
fileInp.value = '';
quizCarryPlaqueUploadPending = Math.max(0, quizCarryPlaqueUploadPending - 1);
if (quizCarryPlaqueUploadPending === 0) quizCarryPlaqueUploadStartMs = 0;
});
});
upWrap.appendChild(document.createTextNode('อัปโหลด'));
upWrap.appendChild(document.createElement('br'));
upWrap.appendChild(fileInp);
upWrap.appendChild(upMsg);
rowImg.appendChild(upWrap);
grid.appendChild(rowImg);
det.appendChild(grid);
root.appendChild(det);
quizCarryPlaqueUpdateNeonUi(si);
}
root.setAttribute('data-mounted', '1');
}
function quizCarryPlaqueComposeFromPickers(si, kind) {
var ids = quizCarryPlaqueRowIds(si, kind);
var sw = el(ids.sw);
var ra = el(ids.ra);
var fb = QUIZ_CARRY_PLAQUE_DEFAULTS_RGBA[kind] || QUIZ_CARRY_PLAQUE_DEFAULTS_RGBA.fill;
if (!sw || !ra) return quizCarryRgbToRgbaString(fb);
var pr = quizCarryParseHexToRgb(sw.value);
if (!pr) return quizCarryRgbToRgbaString(fb);
var pct = parseInt(String(ra.value), 10);
if (!Number.isFinite(pct)) pct = Math.round((fb.a != null ? fb.a : 1) * 100);
pct = Math.max(0, Math.min(100, pct));
return quizCarryRgbToRgbaString({ r: pr.r, g: pr.g, b: pr.b, a: pct / 100 });
}
function quizCarryPlaqueSetPickersFromString(si, kind, cssStr) {
var ids = quizCarryPlaqueRowIds(si, kind);
var fb = QUIZ_CARRY_PLAQUE_DEFAULTS_RGBA[kind] || QUIZ_CARRY_PLAQUE_DEFAULTS_RGBA.fill;
var o = quizCarryParseCssColor(cssStr, fb);
var sw = el(ids.sw);
var ra = el(ids.ra);
var hi = el(ids.hi);
var out = el(ids.out);
if (sw) sw.value = quizCarryHexFromRgb(o.r, o.g, o.b);
if (ra) ra.value = String(Math.round((o.a != null ? o.a : 1) * 100));
if (out) out.textContent = ra ? ra.value + '%' : '';
if (hi) hi.value = quizCarryRgbToRgbaString(o);
}
function quizCarryPlaqueSyncRow(si, kind) {
var s = quizCarryPlaqueComposeFromPickers(si, kind);
var ids = quizCarryPlaqueRowIds(si, kind);
var hi = el(ids.hi);
var out = el(ids.out);
var ra = el(ids.ra);
if (hi) hi.value = s;
if (out && ra) out.textContent = ra.value + '%';
}
var quizCarryPlaqueBindPickersDone = false;
function quizCarryPlaqueBindPickersOnce() {
if (quizCarryPlaqueBindPickersDone) return;
quizCarryPlaqueEnsureMounted();
for (var si = 0; si < QUIZ_CARRY_PLAQUE_SLOT_COUNT; si++) {
QUIZ_CARRY_PLAQUE_KINDS.forEach(function (kind) {
var ids = quizCarryPlaqueRowIds(si, kind);
var sw = el(ids.sw);
var ra = el(ids.ra);
if (sw) {
sw.addEventListener('input', function () {
quizCarryPlaqueSyncRow(si, kind);
});
}
if (ra) {
ra.addEventListener('input', function () {
quizCarryPlaqueSyncRow(si, kind);
});
}
});
}
quizCarryPlaqueBindPickersDone = true;
}
function quizCarryPlaqueBuildForSave() {
quizCarryPlaqueEnsureMounted();
quizCarryPlaqueBindPickersOnce();
var arr = [];
for (var si = 0; si < QUIZ_CARRY_PLAQUE_SLOT_COUNT; si++) {
for (var ki = 0; ki < QUIZ_CARRY_PLAQUE_KINDS.length; ki++) {
quizCarryPlaqueSyncRow(si, QUIZ_CARRY_PLAQUE_KINDS[ki]);
}
var modeEl = el(quizCarryPlaqueSlotPrefix(si) + 'border-mode');
var mode = modeEl && modeEl.value === 'fixed' ? 'fixed' : 'neon';
var bwEl = el(quizCarryPlaqueSlotPrefix(si) + 'border-w');
var bw = bwEl ? parseFloat(String(bwEl.value)) : 2.5;
if (!Number.isFinite(bw)) bw = 2.5;
bw = Math.round(Math.max(0, Math.min(8, bw)) * 10) / 10;
var imgUrlEl = el(quizCarryPlaqueSlotPrefix(si) + 'plaque-image-url');
var plaqueImageUrl = imgUrlEl ? String(imgUrlEl.value || '').trim() : '';
if (!plaqueImageUrl && quizCarryPlaquePendingUrlBySlot[si]) {
plaqueImageUrl = String(quizCarryPlaquePendingUrlBySlot[si] || '').trim();
if (imgUrlEl && plaqueImageUrl) imgUrlEl.value = plaqueImageUrl;
}
plaqueImageUrl = plaqueImageUrl.slice(0, 512);
arr.push({
borderMode: mode,
fixedBorder: quizCarryPlaqueComposeFromPickers(si, 'fixed'),
fillBg: quizCarryPlaqueComposeFromPickers(si, 'fill'),
textColor: quizCarryPlaqueComposeFromPickers(si, 'text'),
borderWidthPx: bw,
plaqueImageUrl: plaqueImageUrl,
});
}
return arr;
}
function quizCarryPlaqueFillSlot(si, t) {
var obj = t && typeof t === 'object' ? t : {};
var modeEl = el(quizCarryPlaqueSlotPrefix(si) + 'border-mode');
if (modeEl) modeEl.value = String(obj.borderMode || '').toLowerCase() === 'fixed' ? 'fixed' : 'neon';
quizCarryPlaqueSetPickersFromString(si, 'fixed', obj.fixedBorder);
quizCarryPlaqueSetPickersFromString(si, 'fill', obj.fillBg);
quizCarryPlaqueSetPickersFromString(si, 'text', obj.textColor);
var bwEl = el(quizCarryPlaqueSlotPrefix(si) + 'border-w');
var bw = parseFloat(String(obj.borderWidthPx));
if (bwEl) {
bwEl.value = String(Number.isFinite(bw) ? Math.round(Math.max(0, Math.min(8, bw)) * 10) / 10 : 2.5);
}
var imgEl = el(quizCarryPlaqueSlotPrefix(si) + 'plaque-image-url');
if (imgEl) {
imgEl.value = obj.plaqueImageUrl != null ? String(obj.plaqueImageUrl) : '';
}
var srvU = obj.plaqueImageUrl != null ? String(obj.plaqueImageUrl).trim() : '';
if (srvU) quizCarryPlaquePendingUrlBySlot[si] = srvU.slice(0, 512);
var msgEl = el(quizCarryPlaqueSlotPrefix(si) + 'plaque-image-msg');
if (msgEl) msgEl.textContent = '';
quizCarryPlaqueUpdateNeonUi(si);
quizCarryPlaqueUpdateImagePreview(si);
}
function quizCarryPlaqueFillAllFromApi(data) {
quizCarryPlaqueBindPickersOnce();
var arr = null;
if (data && Array.isArray(data.carryChoicePlaqueThemes) && data.carryChoicePlaqueThemes.length) {
arr = data.carryChoicePlaqueThemes;
} else if (data && data.carryChoicePlaqueTheme && typeof data.carryChoicePlaqueTheme === 'object') {
arr = [];
for (var u = 0; u < QUIZ_CARRY_PLAQUE_SLOT_COUNT; u++) arr.push(data.carryChoicePlaqueTheme);
}
for (var si = 0; si < QUIZ_CARRY_PLAQUE_SLOT_COUNT; si++) {
var th = (arr && arr[si]) ? arr[si] : {};
quizCarryPlaqueFillSlot(si, th);
quizCarryPlaqueUpdateImagePreview(si);
}
}
function quizCarrySetPlaqueMapScaleInputs(scale) {
var pms = Number(scale);
if (!Number.isFinite(pms)) pms = 1.25;
pms = Math.max(0.85, Math.min(2.5, pms));
var cent = Math.round(pms * 100);
cent = Math.max(85, Math.min(250, cent));
var num = el('quiz-carry-plaque-map-scale');
var rng = el('quiz-carry-plaque-map-scale-range');
var out = el('quiz-carry-plaque-map-scale-out');
if (num) num.value = String(Math.round((cent / 100) * 100) / 100);
if (rng) rng.value = String(cent);
if (out) out.textContent = '× ' + (num ? num.value : String(cent / 100));
}
function wireQuizCarryPlaqueMapScaleControls() {
if (quizCarryPlaqueScaleControlsWired) return;
var num = el('quiz-carry-plaque-map-scale');
var rng = el('quiz-carry-plaque-map-scale-range');
if (!num || !rng) return;
quizCarryPlaqueScaleControlsWired = true;
function fromRange() {
var c = parseInt(String(rng.value), 10);
if (!Number.isFinite(c)) c = 125;
c = Math.max(85, Math.min(250, c));
rng.value = String(c);
var sc = c / 100;
num.value = String(Math.round(sc * 100) / 100);
var out = el('quiz-carry-plaque-map-scale-out');
if (out) out.textContent = '× ' + num.value;
}
function fromNum() {
var p = parseFloat(String(num.value || '').replace(',', '.'));
if (!Number.isFinite(p)) p = 1.25;
p = Math.max(0.85, Math.min(2.5, p));
rng.value = String(Math.round(p * 100));
num.value = String(Math.round(p * 100) / 100);
var out = el('quiz-carry-plaque-map-scale-out');
if (out) out.textContent = '× ' + num.value;
}
rng.addEventListener('input', fromRange);
rng.addEventListener('change', fromRange);
num.addEventListener('input', fromNum);
num.addEventListener('change', fromNum);
}
function loadQuizCarryPanel(opts) {
opts = opts || {};
var delay = Number(opts.fetchDelayMs) || 0;
var runLoad = function () {
gameQuizFetch('GET').then(function (data) {
if (opts.themeOverride && typeof opts.themeOverride === 'object') {
var ov = opts.themeOverride;
if (
ov.panelBg != null ||
ov.borderWidthPx != null ||
ov.panelBorder != null ||
ov.textColor != null ||
ov.questionFontMinPx != null ||
ov.questionFontMaxPx != null
) {
data.carryMapPanelTheme = ov;
} else if (ov.carryMapPanelTheme && typeof ov.carryMapPanelTheme === 'object') {
data.carryMapPanelTheme = ov.carryMapPanelTheme;
}
if (ov.carryEmbedCountdownTheme && typeof ov.carryEmbedCountdownTheme === 'object') {
data.carryEmbedCountdownTheme = ov.carryEmbedCountdownTheme;
}
if (ov.carryChoicePlaqueThemes && Array.isArray(ov.carryChoicePlaqueThemes)) {
data.carryChoicePlaqueThemes = ov.carryChoicePlaqueThemes;
if (ov.carryChoicePlaqueThemes[0] && typeof ov.carryChoicePlaqueThemes[0] === 'object') {
data.carryChoicePlaqueTheme = ov.carryChoicePlaqueThemes[0];
}
} else if (ov.carryChoicePlaqueTheme && typeof ov.carryChoicePlaqueTheme === 'object') {
/* legacy อ็อบเจ็กต์เดียว — ต้องตั้งทั้งอาร์เรย์ ไม่งั้น Fill ยังใช้ carryChoicePlaqueThemes จาก GET เก่า (URL ป้ายหายหลังบันทึก) */
data.carryChoicePlaqueTheme = ov.carryChoicePlaqueTheme;
var expPlaque = [];
for (var exi = 0; exi < QUIZ_CARRY_PLAQUE_SLOT_COUNT; exi++) {
expPlaque.push(ov.carryChoicePlaqueTheme);
}
data.carryChoicePlaqueThemes = expPlaque;
}
}
if (opts.timingOverride && typeof opts.timingOverride === 'object') {
var t = opts.timingOverride;
if (t.carryReadMs != null) data.carryReadMs = t.carryReadMs;
if (t.carryAnswerMs != null) data.carryAnswerMs = t.carryAnswerMs;
if (t.carrySessionLength != null) data.carrySessionLength = t.carrySessionLength;
}
renderQuizCarryAdminList(data.carryQuestions || []);
var readSec = el('quiz-carry-read-sec');
var ansSec = el('quiz-carry-answer-sec');
if (readSec) {
var rms = Number(data.carryReadMs);
readSec.value = String(Number.isFinite(rms) ? Math.round(rms / 1000) : 3);
}
if (ansSec) {
var ams = Number(data.carryAnswerMs);
ansSec.value = String(Number.isFinite(ams) ? Math.round(ams / 1000) : 5);
}
var sessLen = el('quiz-carry-session-len');
if (sessLen) {
var sl = parseInt(String(data.carrySessionLength), 10);
sessLen.value = String(Number.isFinite(sl) && sl >= 0 ? sl : 0);
}
quizCarrySetPlaqueMapScaleInputs(data.carryChoicePlaqueMapScale);
quizCarryBindThemePickersOnce();
var th = data.carryMapPanelTheme && typeof data.carryMapPanelTheme === 'object' ? data.carryMapPanelTheme : {};
quizCarrySetPickersFromColorString('bg', th.panelBg);
quizCarrySetPickersFromColorString('border', th.panelBorder);
quizCarrySetPickersFromColorString('text', th.textColor);
var bwInp = el('quiz-carry-theme-border-w');
if (bwInp) {
var bw = parseInt(String(th.borderWidthPx), 10);
bwInp.value = String(Number.isFinite(bw) && bw >= 0 ? Math.min(12, bw) : 2);
}
var qfMinInp = el('quiz-carry-theme-qfont-min');
var qfMaxInp = el('quiz-carry-theme-qfont-max');
if (qfMinInp) {
var qm = parseInt(String(th.questionFontMinPx), 10);
qfMinInp.value = String(Number.isFinite(qm) ? Math.max(10, Math.min(40, qm)) : 10);
}
if (qfMaxInp) {
var qx = parseInt(String(th.questionFontMaxPx), 10);
qfMaxInp.value = String(Number.isFinite(qx) ? Math.max(14, Math.min(56, qx)) : 24);
}
quizCarryEcdFillForm(data.carryEmbedCountdownTheme || {});
quizCarryPlaqueFillAllFromApi(data);
if (opts.clearMsg !== false) setMsg('quiz-carry-settings-msg', '', '');
}).catch(function (e) {
setMsg('quiz-carry-settings-msg', e.message || 'โหลดไม่ได้', 'error');
});
};
if (delay > 0) setTimeout(runLoad, delay);
else runLoad();
}
/** รอจน plaque upload จบ (pending=0) — กันกดบันทึกทันทีหลังเลือกไฟล์แล้ว build ได้ URL ว่าง · returns false if timeout */
function waitUntilQuizCarryUploadsDone(maxMs) {
maxMs = maxMs || 120000;
return new Promise(function (resolve) {
if (quizCarryPlaqueUploadPending <= 0) return resolve(true);
var started = Date.now();
var iv = setInterval(function () {
if (quizCarryPlaqueUploadPending <= 0) {
clearInterval(iv);
resolve(true);
} else if (Date.now() - started >= maxMs) {
clearInterval(iv);
resolve(false);
}
}, 40);
});
}
function saveQuizCarryPanel() {
var list = el('quiz-carry-admin-list');
var saveBtn = el('btn-quiz-carry-save');
if (!list) {
setMsg('quiz-carry-settings-msg', 'ไม่พบรายการคำถามในแท็บนี้ — รีเฟรชหน้า · quiz-carry-admin-list missing', 'error');
return;
}
if (quizCarryPlaqueUploadPending > 0 && quizCarryPlaqueUploadStartMs > 0 && Date.now() - quizCarryPlaqueUploadStartMs > 120000) {
quizCarryPlaqueUploadPending = 0;
quizCarryPlaqueUploadStartMs = 0;
setMsg(
'quiz-carry-settings-msg',
'ยกเลิกสถานะอัปโหลดค้าง (>2 นาที) แล้วดำเนินการบันทึก — ถ้ายังต้องการรูปให้อัปโหลดใหม่ · Stuck upload cleared; re-upload if needed',
'error',
);
}
if (quizCarrySaveInFlight) {
setMsg('quiz-carry-settings-msg', 'กำลังบันทึกอยู่แล้ว — รอสักครู่ · Save already in progress', 'error');
return;
}
var carryQuestions = [];
list.querySelectorAll('.quiz-carry-admin-block').forEach(function (block) {
var inpQ = block.querySelector('.quiz-carry-q-text');
var text = inpQ && inpQ.value ? String(inpQ.value).trim() : '';
if (!text) return;
var slots = [];
block.querySelectorAll('.quiz-carry-slot-label').forEach(function (lab, idx) {
var ti = lab.querySelector('.quiz-carry-slot-inp');
var ii = lab.querySelector('.quiz-carry-slot-img');
var tv = ti && ti.value ? String(ti.value).trim() : '';
var iv = ii && ii.value ? String(ii.value).trim().slice(0, 512) : '';
if (tv) slots.push({ idx: idx, text: tv, imageUrl: iv });
});
if (slots.length < 2) return;
var sel = block.querySelector('.quiz-carry-correct-sel');
var slotStr = sel && !sel.disabled && sel.value !== '' ? sel.value : null;
if (slotStr == null) return;
var slotNum = parseInt(slotStr, 10);
var correctIndex = -1;
for (var i = 0; i < slots.length; i++) {
if (slots[i].idx === slotNum) {
correctIndex = i;
break;
}
}
if (correctIndex < 0) return;
var rowOut = {
text: text,
choices: slots.map(function (s) {
return s.text;
}),
correctIndex: correctIndex,
};
var urls = slots.map(function (s) {
return s.imageUrl || '';
});
if (urls.some(function (u) {
return u;
})) rowOut.choiceImageUrls = urls;
carryQuestions.push(rowOut);
});
var readSecEl = el('quiz-carry-read-sec');
var ansSecEl = el('quiz-carry-answer-sec');
var readSec = readSecEl ? parseInt(String(readSecEl.value || '3'), 10) : 3;
var ansSec = ansSecEl ? parseInt(String(ansSecEl.value || '5'), 10) : 5;
if (!Number.isFinite(readSec) || readSec < 0) readSec = 0;
if (readSec > 120) readSec = 120;
if (!Number.isFinite(ansSec) || ansSec < 1) ansSec = 1;
if (ansSec > 300) ansSec = 300;
var carryReadMs = readSec * 1000;
var carryAnswerMs = ansSec * 1000;
var sessEl = el('quiz-carry-session-len');
var carrySessionLength = sessEl ? parseInt(String(sessEl.value || '0'), 10) : 0;
if (!Number.isFinite(carrySessionLength) || carrySessionLength < 0) carrySessionLength = 0;
if (carrySessionLength > 500) carrySessionLength = 500;
var carryMapPanelTheme = quizCarryBuildThemeForSave();
var blockEls = list.querySelectorAll('.quiz-carry-admin-block');
var hasAnyQuestionText = false;
for (var bi = 0; bi < blockEls.length; bi++) {
var qn = blockEls[bi].querySelector('.quiz-carry-q-text');
if (qn && String(qn.value || '').trim()) {
hasAnyQuestionText = true;
break;
}
}
if (carryQuestions.length === 0 && hasAnyQuestionText) {
setMsg(
'quiz-carry-settings-msg',
'ยังบันทึกชุดคำถามไม่ได้: แต่ละข้อต้องมีคำถาม ตัวเลือกอย่างน้อย 2 ช่องที่กรอกแล้ว และเลือก «ข้อที่ถูกต้อง» — ตรวจแถวที่มีข้อความค้าง',
'error',
);
return;
}
quizCarrySaveInFlight = true;
if (saveBtn) {
saveBtn.disabled = true;
saveBtn.setAttribute('aria-busy', 'true');
}
var waitLine =
quizCarryPlaqueUploadPending > 0
? 'กำลังรอให้อัปโหลดรูปป้ายจบ แล้วจะบันทึกอัตโนมัติ (ไม่ต้องกดซ้ำ)… · Waiting for plaque upload, then save…'
: 'กำลังบันทึก… · Saving…';
setMsg('quiz-carry-settings-msg', waitLine, '');
try {
var smEl = el('quiz-carry-settings-msg');
if (smEl) smEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
} catch (scrollSave) { /* ignore */ }
waitUntilQuizCarryUploadsDone(120000).then(function (uploadsOk) {
if (!uploadsOk) {
quizCarrySaveInFlight = false;
if (saveBtn) {
saveBtn.disabled = false;
saveBtn.removeAttribute('aria-busy');
}
setMsg(
'quiz-carry-settings-msg',
'รออัปโหลดนานเกิน 2 นาที — ตรวจเครือข่าย/เซิร์ฟเกมแล้วลองใหม่ · Upload wait timeout',
'error',
);
return;
}
var plaqueScaleEl = el('quiz-carry-plaque-map-scale');
var plaqueMapScale = 1.25;
if (plaqueScaleEl) {
var psn = parseFloat(String(plaqueScaleEl.value || '').replace(',', '.'));
if (Number.isFinite(psn)) plaqueMapScale = Math.max(0.85, Math.min(2.5, psn));
}
var putBody = {
carryReadMs: carryReadMs,
carryAnswerMs: carryAnswerMs,
carrySessionLength: carrySessionLength,
carryMapPanelTheme: carryMapPanelTheme,
carryEmbedCountdownTheme: quizCarryEcdBuildForSave(),
carryChoicePlaqueThemes: quizCarryPlaqueBuildForSave(),
carryChoicePlaqueMapScale: plaqueMapScale,
};
if (carryQuestions.length > 0) {
putBody.carryQuestions = carryQuestions;
}
gameQuizFetch('PUT', putBody).then(function (res) {
var tail = carryQuestions.length ? ' · คำถาม ' + carryQuestions.length + ' ข้อ' : ' · คงชุดคำถามเดิม (อัปเดตเวลา/ธีมอย่างเดียว)';
var themeFromRes = res && res.carryMapPanelTheme && typeof res.carryMapPanelTheme === 'object' ? res.carryMapPanelTheme : null;
var ecdFromRes = res && res.carryEmbedCountdownTheme && typeof res.carryEmbedCountdownTheme === 'object' ? res.carryEmbedCountdownTheme : null;
var plaqueFromRes = null;
if (res && Array.isArray(res.carryChoicePlaqueThemes) && res.carryChoicePlaqueThemes.length) {
plaqueFromRes = res.carryChoicePlaqueThemes;
} else if (res && res.carryChoicePlaqueTheme && typeof res.carryChoicePlaqueTheme === 'object') {
plaqueFromRes = [];
for (var pxi = 0; pxi < QUIZ_CARRY_PLAQUE_SLOT_COUNT; pxi++) {
plaqueFromRes.push(res.carryChoicePlaqueTheme);
}
}
if (!plaqueFromRes) {
plaqueFromRes = quizCarryPlaqueBuildForSave();
}
var themeOk = !!themeFromRes || !!ecdFromRes || !!plaqueFromRes;
var themeTail = themeOk ? ' · ธีมแผง/ป้าย/นับถอยหลังบันทึกแล้ว' : '';
setMsg('quiz-carry-settings-msg', 'บันทึกแล้ว' + tail + themeTail, 'ok');
quizCarryPlaquePendingUrlBySlot = {};
loadQuizCarryPanel({
clearMsg: false,
themeOverride: {
carryMapPanelTheme: themeFromRes || carryMapPanelTheme,
carryEmbedCountdownTheme: ecdFromRes || quizCarryEcdBuildForSave(),
carryChoicePlaqueThemes: Array.isArray(plaqueFromRes) ? plaqueFromRes : null,
carryChoicePlaqueTheme:
Array.isArray(plaqueFromRes) && plaqueFromRes[0] && typeof plaqueFromRes[0] === 'object'
? plaqueFromRes[0]
: null,
},
timingOverride: {
carryReadMs: carryReadMs,
carryAnswerMs: carryAnswerMs,
carrySessionLength: carrySessionLength,
},
fetchDelayMs: 120,
});
}).catch(function (e) {
setMsg('quiz-carry-settings-msg', e.message || 'บันทึกไม่ได้', 'error');
}).finally(function () {
quizCarrySaveInFlight = false;
if (saveBtn) {
saveBtn.disabled = false;
saveBtn.removeAttribute('aria-busy');
}
});
});
}
/** หมวด Quiz Battle — ชื่อไทยให้สอดคล้องธีมการศึกษาไซเบอร์ / หน้า Quiz-Battle */
var QB_LABELS_ABC = ['A', 'B', 'C'];
var QUIZ_BATTLE_CATEGORIES = [
{ id: 'cybercrime', labelTh: 'อาชญากรรมออนไลน์', labelEn: 'Cybercrime' },
{ id: 'cyber_law', labelTh: 'กฎหมายไซเบอร์', labelEn: 'Cyber law' },
{ id: 'privacy', labelTh: 'ความเป็นส่วนตัวและข้อมูลส่วนบุคคล', labelEn: 'Privacy & PDPA' },
{ id: 'digital_security', labelTh: 'ความปลอดภัยดิจิทัล', labelEn: 'Digital security' },
{ id: 'social_media', labelTh: 'สื่อสังคมออนไลน์', labelEn: 'Social media' },
{ id: 'ethics', labelTh: 'จริยธรรมดิจิทัล', labelEn: 'Digital ethics' },
{ id: 'ai_data', labelTh: 'AI และข้อมูล', labelEn: 'AI & data' },
];
function qbBattleCategoryLabel(catId) {
for (var i = 0; i < QUIZ_BATTLE_CATEGORIES.length; i++) {
if (QUIZ_BATTLE_CATEGORIES[i].id === catId) return QUIZ_BATTLE_CATEGORIES[i].labelTh;
}
return 'Quiz Battle';
}
function fillQbBattleFilterOptions() {
var sel = el('qb-battle-filter-cat');
if (!sel) return;
var keep = sel.value;
while (sel.options.length > 1) sel.remove(1);
QUIZ_BATTLE_CATEGORIES.forEach(function (c) {
var o = document.createElement('option');
o.value = c.id;
o.textContent = c.labelTh;
sel.appendChild(o);
});
if (keep) {
for (var oi = 0; oi < sel.options.length; oi++) {
if (sel.options[oi].value === keep) {
sel.value = keep;
break;
}
}
}
}
function applyQbBattleFilter() {
var list = el('qb-battle-admin-list');
var f = el('qb-battle-filter-cat');
if (!list || !f) return;
var fv = f.value || '';
list.querySelectorAll('.qb-battle-block').forEach(function (row) {
var catInp = row.querySelector('.qb-battle-cat');
var cat = catInp && catInp.value ? catInp.value : '';
row.hidden = !!(fv && cat !== fv);
});
refreshQbBattlePreview();
}
function getFirstVisibleQbBattleBlock() {
var list = el('qb-battle-admin-list');
if (!list) return null;
var rows = list.querySelectorAll('.qb-battle-block');
for (var i = 0; i < rows.length; i++) {
if (rows[i].hidden) continue;
var q = rows[i].querySelector('.qb-battle-q-text');
var t = q && q.value ? String(q.value).trim() : '';
if (t) return rows[i];
}
for (var j = 0; j < rows.length; j++) {
if (!rows[j].hidden) return rows[j];
}
return null;
}
function readQbBattleBlockData(row) {
if (!row) return null;
var catSel = row.querySelector('.qb-battle-cat');
var qInp = row.querySelector('.qb-battle-q-text');
var a0 = row.querySelector('.qb-battle-a');
var a1 = row.querySelector('.qb-battle-b');
var a2 = row.querySelector('.qb-battle-c');
var radios = row.querySelectorAll('input[type="radio"][name^="qb-correct-"]');
var ci = 0;
for (var i = 0; i < radios.length; i++) {
if (radios[i].checked) {
ci = parseInt(radios[i].value, 10);
if (!Number.isFinite(ci)) ci = 0;
break;
}
}
return {
categoryId: catSel && catSel.value ? catSel.value : 'cybercrime',
text: qInp && qInp.value ? String(qInp.value).trim() : '',
choices: [
a0 && a0.value ? String(a0.value).trim() : '',
a1 && a1.value ? String(a1.value).trim() : '',
a2 && a2.value ? String(a2.value).trim() : '',
],
correctIndex: Math.max(0, Math.min(2, ci)),
};
}
function refreshQbBattlePreview() {
var hudCat = el('qb-preview-hud-cat');
var qtext = el('qb-preview-modal-q');
var c0 = el('qb-preview-c0');
var c1 = el('qb-preview-c1');
var c2 = el('qb-preview-c2');
var qModal = el('qb-cyber-modal-question');
var sModal = el('qb-cyber-modal-summary');
var layer = el('qb-modal-layer');
if (!hudCat || !qtext || !c0 || !c1 || !c2 || !qModal || !sModal) return;
var modeBtn = document.querySelector('.qb-preview-mode-btn.is-active');
var mode = modeBtn && modeBtn.getAttribute('data-qb-mode') ? modeBtn.getAttribute('data-qb-mode') : 'question';
var row = getFirstVisibleQbBattleBlock();
var d = readQbBattleBlockData(row);
var defQ = 'การกระทำใดผิดกฎหมายไซเบอร์?';
var defC = [
'การเจาะระบบเข้าสู่ข้อมูลส่วนบุคคล',
'การใช้ WI-FI สาธารณะเพื่อดูข่าว',
'การอัปเดตซอฟต์แวร์เพื่อความปลอดภัย',
];
hudCat.textContent = d ? qbBattleCategoryLabel(d.categoryId) : qbBattleCategoryLabel('cybercrime');
qtext.textContent = d && d.text ? d.text : defQ;
var ch = d ? d.choices : defC;
for (var s = 0; s < 3; s++) {
var eln = s === 0 ? c0 : s === 1 ? c1 : c2;
var txt = ch[s] ? ch[s] : defC[s];
eln.textContent = QB_LABELS_ABC[s] + ' : ' + txt;
}
var choiceEls = document.querySelectorAll('#qb-preview-choices .qb-cyber-choice');
choiceEls.forEach(function (node) {
node.classList.remove('qb-cyber-choice--correct', 'qb-cyber-choice--wrong', 'qb-cyber-choice--neutral');
var mark = node.querySelector('.qb-cyber-choice-mark');
if (mark) mark.textContent = '';
});
if (mode === 'summary') {
qModal.hidden = true;
sModal.hidden = false;
if (layer) layer.classList.add('qb-modal-layer--summary');
} else {
qModal.hidden = false;
sModal.hidden = true;
if (layer) layer.classList.remove('qb-modal-layer--summary');
var ci = d ? d.correctIndex : 0;
if (mode === 'revealed') {
var wrongPick = (ci + 1) % 3;
choiceEls.forEach(function (node, idx) {
var mark = node.querySelector('.qb-cyber-choice-mark');
if (idx === ci) {
node.classList.add('qb-cyber-choice--correct');
if (mark) mark.textContent = '✓';
} else if (idx === wrongPick) {
node.classList.add('qb-cyber-choice--wrong');
if (mark) mark.textContent = '✕';
} else {
node.classList.add('qb-cyber-choice--neutral');
}
});
}
}
}
function addQbBattleRow(q) {
var list = el('qb-battle-admin-list');
if (!list) return;
q = q || {};
var uid = 'r' + Math.random().toString(36).slice(2, 10);
var block = document.createElement('div');
block.className = 'qb-battle-block';
block.setAttribute('data-category', q.categoryId || 'cybercrime');
var head = document.createElement('div');
head.className = 'qb-battle-block-head';
var labCat = document.createElement('label');
labCat.className = 'qb-battle-cat-label';
labCat.appendChild(document.createTextNode('หมวด'));
var selCat = document.createElement('select');
selCat.className = 'qb-battle-cat';
QUIZ_BATTLE_CATEGORIES.forEach(function (c) {
var o = document.createElement('option');
o.value = c.id;
o.textContent = c.labelTh;
if ((q.categoryId || 'cybercrime') === c.id) o.selected = true;
selCat.appendChild(o);
});
selCat.addEventListener('change', function () {
block.setAttribute('data-category', selCat.value);
applyQbBattleFilter();
});
labCat.appendChild(selCat);
var rm = document.createElement('button');
rm.type = 'button';
rm.className = 'btn btn-danger btn-sm qb-battle-remove';
rm.textContent = 'ลบข้อ';
rm.addEventListener('click', function () {
block.remove();
if (!list.querySelector('.qb-battle-block')) addQbBattleRow(null);
applyQbBattleFilter();
});
head.appendChild(labCat);
head.appendChild(rm);
block.appendChild(head);
var labQ = document.createElement('label');
labQ.className = 'qb-battle-q-label';
labQ.appendChild(document.createTextNode('คำถาม'));
var inpQ = document.createElement('input');
inpQ.type = 'text';
inpQ.className = 'qb-battle-q-text';
inpQ.maxLength = 500;
inpQ.placeholder = 'พิมพ์คำถาม…';
inpQ.value = q.text ? String(q.text) : '';
labQ.appendChild(inpQ);
block.appendChild(labQ);
var choices = Array.isArray(q.choices) ? q.choices : [];
var grid = document.createElement('div');
grid.className = 'qb-battle-abc-grid';
[['A', 'a', 0], ['B', 'b', 1], ['C', 'c', 2]].forEach(function (tri) {
var lab = document.createElement('label');
lab.className = 'qb-battle-opt-label';
lab.appendChild(document.createTextNode('ตัวเลือก ' + tri[0]));
var inp = document.createElement('input');
inp.type = 'text';
inp.className = 'qb-battle-' + tri[1];
inp.maxLength = 160;
inp.placeholder = tri[0];
inp.value = choices[tri[2]] != null ? String(choices[tri[2]]) : '';
lab.appendChild(inp);
grid.appendChild(lab);
});
block.appendChild(grid);
var ci = Number(q.correctIndex);
if (!Number.isFinite(ci)) ci = 0;
ci = Math.max(0, Math.min(2, Math.floor(ci)));
var fs = document.createElement('fieldset');
fs.className = 'qb-battle-correct-fs';
var leg = document.createElement('legend');
leg.textContent = 'ข้อที่ถูกต้อง';
fs.appendChild(leg);
var rg = document.createElement('div');
rg.className = 'qb-battle-correct-rg';
for (var r = 0; r < 3; r++) {
var labR = document.createElement('label');
labR.className = 'qb-battle-radio-label';
var radio = document.createElement('input');
radio.type = 'radio';
radio.name = 'qb-correct-' + uid;
radio.value = String(r);
if (r === ci) radio.checked = true;
labR.appendChild(radio);
labR.appendChild(document.createTextNode(' ' + QB_LABELS_ABC[r]));
rg.appendChild(labR);
}
fs.appendChild(rg);
block.appendChild(fs);
function onFieldInput() {
refreshQbBattlePreview();
}
inpQ.addEventListener('input', onFieldInput);
grid.querySelectorAll('input').forEach(function (inp) {
inp.addEventListener('input', onFieldInput);
});
rg.querySelectorAll('input[type="radio"]').forEach(function (rdo) {
rdo.addEventListener('change', onFieldInput);
});
list.appendChild(block);
applyQbBattleFilter();
}
function renderQbBattleList(arr, clearMsg) {
var list = el('qb-battle-admin-list');
if (!list) return;
list.innerHTML = '';
fillQbBattleFilterOptions();
var rows = arr && arr.length ? arr : [null];
rows.forEach(function (q) {
addQbBattleRow(q);
});
if (clearMsg !== false) setMsg('qb-battle-settings-msg', '', '');
}
function loadQbBattlePanel(keepSavedMsg) {
gameQuizFetch('GET').then(function (data) {
renderQbBattleList(data.battleQuizMcq || [], !keepSavedMsg);
refreshQbBattlePreview();
}).catch(function (e) {
setMsg('qb-battle-settings-msg', e.message || 'โหลดไม่ได้', 'error');
});
}
function classifyQbBattleRow(block) {
var d = readQbBattleBlockData(block);
if (!d) return { kind: 'empty', d: null };
var hasQ = !!d.text;
var filled = 0;
for (var i = 0; i < d.choices.length; i++) {
if (d.choices[i]) filled++;
}
if (!hasQ && filled === 0) return { kind: 'empty', d: d };
if (hasQ && filled === 3) return { kind: 'ok', d: d };
return { kind: 'partial', d: d };
}
function saveQbBattlePanel() {
var list = el('qb-battle-admin-list');
if (!list) return;
var battleQuizMcq = [];
var partialLines = [];
var blocks = list.querySelectorAll('.qb-battle-block');
for (var bi = 0; bi < blocks.length; bi++) {
var block = blocks[bi];
var st = classifyQbBattleRow(block);
if (st.kind === 'empty') continue;
if (st.kind === 'partial') {
partialLines.push('แถวที่ ' + (bi + 1) + ': ต้องกรอกคำถามและตัวเลือก A, B, C ให้ครบทุกช่อง');
continue;
}
battleQuizMcq.push({
categoryId: st.d.categoryId,
text: st.d.text,
choices: st.d.choices,
correctIndex: st.d.correctIndex,
});
}
if (partialLines.length) {
setMsg(
'qb-battle-settings-msg',
'ยังบันทึกไม่ได้ — ' + partialLines.join(' · ') + ' · (Empty question/options are skipped by the server; fill all fields.)',
'error'
);
return;
}
gameQuizFetch('PUT', { battleQuizMcq: battleQuizMcq }).then(function (res) {
var n = battleQuizMcq.length;
var savedN = res && typeof res.battleQuizMcqSaved === 'number' ? res.battleQuizMcqSaved : n;
if (n > 0 && savedN !== n) {
setMsg(
'qb-battle-settings-msg',
'เซิร์ฟเวอร์รับเพียง ' + savedN + ' จาก ' + n + ' ข้อ (ข้อที่ไม่ผ่านกฎถูกตัด) · Server kept ' + savedN + '/' + n + ' — ตรวจหมวด/คำถาม/A B C ให้ครบ · Restart Node (Game/server.js) ถ้ายังผิดปกติ',
'error'
);
} else {
setMsg(
'qb-battle-settings-msg',
n ? ('บันทึกแล้ว ' + n + ' ข้อ (ดิสก์ + Node) · Saved ' + n + ' question(s)') : 'บันทึกแล้ว (0 ข้อ) · Saved empty set',
'ok'
);
}
return loadQbBattlePanel(true);
}).catch(function (e) {
setMsg('qb-battle-settings-msg', e.message || 'บันทึกไม่ได้', 'error');
});
}
function laneUrlsToText(arr) {
if (!arr || !arr.length) return '';
return arr.map(function (u) { return String(u || '').trim(); }).filter(Boolean).join('\n');
}
function parseLaneUrlsTextarea() {
var raw = el('game-timing-lane-urls') && el('game-timing-lane-urls').value ? String(el('game-timing-lane-urls').value) : '';
var lines = raw.split(/[\n\r]+/);
var out = [];
for (var i = 0; i < lines.length && out.length < 24; i++) {
var t = lines[i].trim();
if (t) out.push(t);
}
return out;
}
function moveLaneUrl(index, dir) {
var urls = parseLaneUrlsTextarea();
var j = index + dir;
if (j < 0 || j >= urls.length) return;
var t = urls[index];
urls[index] = urls[j];
urls[j] = t;
var ta = el('game-timing-lane-urls');
if (ta) ta.value = urls.join('\n');
renderGameTimingLaneVisuals();
}
function renderGameTimingLaneVisuals() {
var wrap = el('game-timing-lane-visual-list');
if (!wrap) return;
wrap.innerHTML = '';
var urls = parseLaneUrlsTextarea();
if (!urls.length) {
var ph = document.createElement('p');
ph.className = 'muted';
ph.style.margin = '0';
ph.textContent = 'ยังไม่มีรูป — กด «Lane» ที่การ์ดในคลัง · No lane images yet';
wrap.appendChild(ph);
return;
}
urls.forEach(function (u, i) {
var card = document.createElement('div');
card.className = 'game-timing-visual-card';
card.setAttribute('role', 'listitem');
var img = document.createElement('img');
img.className = 'game-timing-visual-card-img';
img.src = u;
img.alt = '';
img.referrerPolicy = 'no-referrer';
img.onerror = function () { img.classList.add('is-broken'); };
var actions = document.createElement('div');
actions.className = 'game-timing-visual-card-actions';
function mkBtn(cls, text, fn) {
var b = document.createElement('button');
b.type = 'button';
b.className = 'btn btn-sm ' + cls;
b.textContent = text;
b.addEventListener('click', fn);
return b;
}
actions.appendChild(mkBtn('btn-ghost', 'ดู', function () {
window.open(u, '_blank', 'noopener,noreferrer');
}));
actions.appendChild(mkBtn('btn-ghost', '←', function () {
moveLaneUrl(i, -1);
}));
actions.appendChild(mkBtn('btn-ghost', '→', function () {
moveLaneUrl(i, 1);
}));
actions.appendChild(mkBtn('btn-danger', 'ลบ', function () {
var next = urls.filter(function (_, j) { return j !== i; });
var ta = el('game-timing-lane-urls');
if (ta) ta.value = next.join('\n');
renderGameTimingLaneVisuals();
setGauntletUploadMsg('เอาออกจาก Lane แล้ว (ไฟล์ในคลังยังอยู่) · Removed from lane', 'ok');
}));
card.appendChild(img);
card.appendChild(actions);
wrap.appendChild(card);
});
}
function renderGameTimingLaserVisuals() {
var slots = [
{ inpId: 'game-timing-laser-top-url', prevId: 'game-timing-laser-top-preview' },
{ inpId: 'game-timing-laser-bottom-url', prevId: 'game-timing-laser-bottom-preview' },
{ inpId: 'game-timing-laser-line-url', prevId: 'game-timing-laser-line-preview' },
];
slots.forEach(function (s) {
var inp = el(s.inpId);
var prev = el(s.prevId);
if (!prev) return;
prev.innerHTML = '';
var url = inp && inp.value ? String(inp.value).trim() : '';
if (!url) {
var ph = document.createElement('div');
ph.className = 'game-timing-laser-placeholder';
ph.textContent = 'ยังไม่ตั้ง · Not set';
prev.appendChild(ph);
return;
}
var img = document.createElement('img');
img.className = 'game-timing-laser-preview-img';
img.src = url;
img.alt = '';
img.referrerPolicy = 'no-referrer';
img.onerror = function () { img.classList.add('is-broken'); };
prev.appendChild(img);
});
}
function bindGameTimingAssignedVisuals() {
function wire(which, viewId, clearId) {
var inpId = 'game-timing-laser-' + which + '-url';
var vBtn = el(viewId);
var cBtn = el(clearId);
if (vBtn) vBtn.addEventListener('click', function () {
var inp = el(inpId);
var u = inp && inp.value ? String(inp.value).trim() : '';
if (!u) return;
window.open(u, '_blank', 'noopener,noreferrer');
});
if (cBtn) cBtn.addEventListener('click', function () {
var inp = el(inpId);
if (inp) inp.value = '';
renderGameTimingLaserVisuals();
setGauntletUploadMsg('ล้างช่อง Laser แล้ว · Laser slot cleared', 'ok');
});
}
wire('top', 'btn-game-timing-laser-top-view', 'btn-game-timing-laser-top-clear');
wire('bottom', 'btn-game-timing-laser-bottom-view', 'btn-game-timing-laser-bottom-clear');
wire('line', 'btn-game-timing-laser-line-view', 'btn-game-timing-laser-line-clear');
}
function gauntletAssetsJsonFetch(url, opts) {
opts = opts || {};
opts.credentials = 'include';
return fetch(url, opts).then(function (r) {
return r.text().then(function (text) {
var j = {};
var parsed = false;
try {
j = text ? JSON.parse(text) : {};
parsed = true;
} catch (e) {
if (!r.ok) {
var snippet = (text || '').replace(/\s+/g, ' ').trim().slice(0, 220);
var tail = '';
if (r.status === 413) {
tail =
' · คำขอใหญ่เกิน (nginx client_max_body_size / PHP post_max_size) — รัน deploy-nginx และตั้ง post_max_size ≥20M · Payload too large';
} else if (r.status === 401) {
tail = ' · ล็อกอิน Admin ใหม่ · Re-login to Admin';
} else if (r.status === 502 || r.status === 503 || r.status === 504) {
tail = ' · เซิร์ฟเกม/PHP upstream ล่ม — ตรวจ Node + php-fpm · Upstream error';
} else if (r.status === 404) {
tail = ' · ตรวจ path ไฟล์ / รีสตาร์ทบริการหลัง deploy · Not found';
}
throw new Error((snippet || r.statusText || 'Request failed') + tail + ' (HTTP ' + r.status + ')');
}
throw e;
}
if (!r.ok) {
var plain = (j && j.error) || r.statusText || 'Request failed';
if (r.status === 413) {
plain =
(j && j.error) ||
'ไฟล์/คำขอใหญ่เกินขีดจำกัด (nginx หรือ PHP) — ตั้ง client_max_body_size (server หรือ /Admin/api/) และ post_max_size ≥20M · Payload too large';
}
if (r.status === 401) {
plain =
(j && j.error) ||
'หมดเซสชันแอดมิน — ล็อกอินใหม่แล้วลองอัปโหลดอีกครั้ง · Re-login to Admin and retry upload';
}
if (r.status === 404) {
plain += ' — รีสตาร์ท Node ที่รัน Game/server.js หลัง deploy · Restart Game Node';
}
if (r.status === 502 || r.status === 503 || r.status === 504) {
plain +=
' — PHP/Node upstream ล่ม — ตรวจ game-zep (Node) และ php-fpm · Check Node + PHP-FPM';
}
if (parsed && (!j || Object.keys(j).length === 0)) {
plain = (text || '').replace(/\s+/g, ' ').trim().slice(0, 200) || plain;
if (r.status) plain += ' (HTTP ' + r.status + ')';
}
throw new Error(plain);
}
return j;
});
});
}
function setGauntletUploadMsg(text, cls) {
setMsg('gauntlet-asset-upload-msg', text, cls);
}
function readFileAsDataURL(file) {
return new Promise(function (resolve, reject) {
var fr = new FileReader();
fr.onload = function () { resolve(fr.result); };
fr.onerror = function () { reject(new Error('อ่านไฟล์ไม่ได้ · Cannot read file')); };
fr.readAsDataURL(file);
});
}
function appendLaneUrlUnique(url) {
var ta = el('game-timing-lane-urls');
if (!ta || !url) return;
var lines = parseLaneUrlsTextarea();
if (lines.indexOf(url) >= 0) {
setGauntletUploadMsg('มี URL นี้ใน Lane แล้ว · Already in lane list', 'ok');
return;
}
lines.push(url);
ta.value = lines.join('\n');
renderGameTimingLaneVisuals();
setGauntletUploadMsg('เพิ่มใน Lane แล้ว · Added to lane URLs', 'ok');
}
function setLaserUrlField(which, url) {
var ids = { top: 'game-timing-laser-top-url', bottom: 'game-timing-laser-bottom-url', line: 'game-timing-laser-line-url' };
var inp = el(ids[which]);
if (!inp) return;
inp.value = url;
renderGameTimingLaserVisuals();
setGauntletUploadMsg('ตั้งค่า Laser แล้ว · Laser field updated', 'ok');
}
function uploadGauntletDataUrls(fileArr) {
var files = [].slice.call(fileArr || []);
if (!files.length) return Promise.resolve();
var rf = gauntletReplaceTarget;
gauntletReplaceTarget = null;
if (rf) files = files.slice(0, 1);
setGauntletUploadMsg('กำลังอัปโหลด… · Uploading…', '');
var idx = 0;
function step() {
if (idx >= files.length) {
setGauntletUploadMsg(rf ? 'แทนที่รูปแล้ว · Image replaced' : ('อัปโหลดแล้ว ' + files.length + ' ไฟล์ · Uploaded'), 'ok');
return refreshGauntletAssetLibrary();
}
var file = files[idx++];
return readFileAsDataURL(file).then(function (dataUrl) {
var body = { imageDataUrl: dataUrl };
if (rf) body.replaceFilename = rf;
else if (file.name) body.label = String(file.name).replace(/\.[^.]+$/, '').slice(0, 120);
return gauntletAssetsJsonFetch(GAME_GAUNTLET_ASSETS_API + '/upload', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
}).then(step).catch(function (e) {
setGauntletUploadMsg(e.message || 'อัปโหลดไม่สำเร็จ · Upload failed', 'error');
return refreshGauntletAssetLibrary();
});
}
return step();
}
function refreshGauntletAssetLibrary() {
var listEl = el('gauntlet-asset-list');
if (!listEl) return Promise.resolve();
return gauntletAssetsJsonFetch(GAME_GAUNTLET_ASSETS_API, { method: 'GET' })
.then(function (data) {
listEl.innerHTML = '';
var items = data.items || [];
if (!items.length) {
var empty = document.createElement('p');
empty.className = 'muted';
empty.style.margin = '0';
empty.textContent = 'ยังไม่มีรูปในคลัง — อัปโหลดด้านบน · No assets yet — upload above';
listEl.appendChild(empty);
return;
}
items.forEach(function (it, ix) {
var card = document.createElement('article');
card.className = 'gauntlet-asset-card';
var img = document.createElement('img');
img.className = 'gauntlet-asset-card-preview';
img.src = it.url + '?t=' + (it.mtime || 0);
img.alt = it.label || '';
var labelWrap = document.createElement('div');
labelWrap.className = 'gauntlet-asset-card-label-wrap';
var labelInp = document.createElement('input');
labelInp.type = 'text';
labelInp.maxLength = 120;
labelInp.value = it.label || '';
var lid = 'gauntlet-asset-label-' + ix;
labelInp.id = lid;
var lab = document.createElement('label');
lab.htmlFor = lid;
lab.textContent = 'ชื่อเรียก · Label';
labelWrap.appendChild(lab);
labelWrap.appendChild(labelInp);
var urlP = document.createElement('p');
urlP.className = 'gauntlet-asset-card-url';
urlP.textContent = it.url;
var actions = document.createElement('div');
actions.className = 'gauntlet-asset-card-actions';
function mkBtn(cls, text, handler) {
var b = document.createElement('button');
b.type = 'button';
b.className = 'btn ' + cls;
b.textContent = text;
b.addEventListener('click', handler);
return b;
}
actions.appendChild(mkBtn('btn-ghost', 'ดูรูป', function () {
window.open(it.url, '_blank', 'noopener,noreferrer');
}));
actions.appendChild(mkBtn('btn-ghost', 'บันทึกชื่อ', function () {
gauntletAssetsJsonFetch(GAME_GAUNTLET_ASSETS_API, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filename: it.filename, label: labelInp.value }),
})
.then(function () {
setGauntletUploadMsg('บันทึกชื่อแล้ว · Label saved', 'ok');
return refreshGauntletAssetLibrary();
})
.catch(function (e) {
setGauntletUploadMsg(e.message, 'error');
});
}));
actions.appendChild(mkBtn('btn-ghost', 'แทนที่รูป', function () {
gauntletReplaceTarget = it.filename;
var finp = el('gauntlet-asset-file-input');
if (finp) finp.click();
}));
actions.appendChild(mkBtn('btn-ghost', 'Lane', function () {
appendLaneUrlUnique(it.url);
}));
actions.appendChild(mkBtn('btn-ghost', 'Laser บน', function () {
setLaserUrlField('top', it.url);
}));
actions.appendChild(mkBtn('btn-ghost', 'Laser ล่าง', function () {
setLaserUrlField('bottom', it.url);
}));
actions.appendChild(mkBtn('btn-ghost', 'Laser เส้น', function () {
setLaserUrlField('line', it.url);
}));
actions.appendChild(mkBtn('btn-danger', 'ลบ', function () {
if (!confirm('ลบไฟล์ ' + it.filename + ' จากเซิร์ฟเวอร์? · Delete from server?')) return;
gauntletAssetsJsonFetch(GAME_GAUNTLET_ASSETS_API + '?file=' + encodeURIComponent(it.filename), { method: 'DELETE' })
.then(function () {
setGauntletUploadMsg('ลบแล้ว · Deleted', 'ok');
return refreshGauntletAssetLibrary();
})
.catch(function (e) {
setGauntletUploadMsg(e.message, 'error');
});
}));
card.appendChild(img);
card.appendChild(labelWrap);
card.appendChild(urlP);
card.appendChild(actions);
listEl.appendChild(card);
});
})
.catch(function (e) {
listEl.innerHTML = '';
var p = document.createElement('p');
p.className = 'msg error';
p.textContent = e.message || 'โหลดคลังไม่ได้';
listEl.appendChild(p);
});
}
function bindGauntletAssetLibrary() {
var drop = el('gauntlet-asset-drop');
var finp = el('gauntlet-asset-file-input');
if (!drop || !finp) return;
drop.addEventListener('click', function () {
finp.click();
});
drop.addEventListener('keydown', function (e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
finp.click();
}
});
['dragenter', 'dragover'].forEach(function (ev) {
drop.addEventListener(ev, function (e) {
e.preventDefault();
e.stopPropagation();
drop.classList.add('is-dragover');
});
});
['dragleave', 'drop'].forEach(function (ev) {
drop.addEventListener(ev, function (e) {
e.preventDefault();
e.stopPropagation();
if (ev !== 'drop') drop.classList.remove('is-dragover');
});
});
drop.addEventListener('drop', function (e) {
drop.classList.remove('is-dragover');
var files = e.dataTransfer && e.dataTransfer.files;
if (!files || !files.length) return;
var arr = [];
for (var i = 0; i < files.length; i++) arr.push(files[i]);
uploadGauntletDataUrls(arr);
});
finp.addEventListener('change', function () {
if (!finp.files || !finp.files.length) return;
var arr = [];
for (var i = 0; i < finp.files.length; i++) arr.push(finp.files[i]);
finp.value = '';
uploadGauntletDataUrls(arr);
});
}
/** ว่าง / อัตโนมัติ / auto → null · ตัวเลข 0.853.2 tile */
function parseStackBlockWidthForSave(raw) {
var t = String(raw == null ? '' : raw).trim();
if (!t) return { ok: true, value: null };
var low = t.toLowerCase();
if (t === 'อัตโนมัติ' || low === 'auto' || low === 'automatic') return { ok: true, value: null };
var w = parseFloat(t.replace(',', '.'));
if (!Number.isFinite(w)) {
return { ok: false, error: 'กว้างบล็อก: ใส่ตัวเลข 0.85–3.2 หรือปล่อยว่างเพื่ออัตโนมัติ' };
}
if (w < 0.85 || w > 3.2) {
return { ok: false, error: 'กว้างบล็อกต้องอยู่ระหว่าง 0.85–3.2 tile หรือปล่อยว่าง' };
}
return { ok: true, value: Math.round(Math.max(0.85, Math.min(3.2, w)) * 100) / 100 };
}
/** ช่อง Hz ใช้ type=text — ปล่อยว่าง = 0.55 */
function parseStackSwingHzForSave(raw) {
var t = String(raw == null ? '' : raw).trim().replace(',', '.');
if (!t) return { ok: true, value: 0.55 };
var h = parseFloat(t);
if (!Number.isFinite(h)) {
return { ok: false, error: 'สวิง Hz: ใส่ตัวเลข (เช่น 0.58) หรือปล่อยว่างให้เป็น 0.55' };
}
h = Math.max(0.08, Math.min(2.8, h));
return { ok: true, value: Math.round(h * 100) / 100 };
}
/** ซิงก์ช่อง Stack จากข้อมูลเซิร์ฟเวอร์ (ช่องอยู่ใน DOM แม้แท็บอื่นเปิดอยู่ — ต้องไม่ปล่อยให้ค้างค่า default จาก HTML) */
function applyGameTimingDataToStackInputs(data) {
if (!data || typeof data !== 'object') return;
if (el('stack-swing-hz')) {
var hz = data.stackSwingHz != null && data.stackSwingHz !== '' ? Number(data.stackSwingHz) : NaN;
if (!Number.isFinite(hz)) hz = 0.55;
hz = Math.max(0.08, Math.min(2.8, hz));
el('stack-swing-hz').value = String(Math.round(hz * 100) / 100);
}
if (el('stack-block-width-tiles')) {
var rawW = data.stackBlockWidthTiles;
var bw = rawW != null && rawW !== '' ? Number(rawW) : NaN;
el('stack-block-width-tiles').value = Number.isFinite(bw) && bw >= 0.85 ? String(Math.round(bw * 100) / 100) : '';
}
}
function loadStackGamePanel() {
gameTimingFetch('GET')
.then(function (data) {
applyGameTimingDataToStackInputs(data);
setMsg('stack-game-msg', '', '');
})
.catch(function (e) {
setMsg('stack-game-msg', e.message || 'โหลดไม่ได้', 'error');
});
}
function saveStackGamePanel() {
if (stackGameSaveInFlight) return;
var hzParsed = parseStackSwingHzForSave(el('stack-swing-hz') ? el('stack-swing-hz').value : '');
if (!hzParsed.ok) {
setMsg('stack-game-msg', hzParsed.error, 'error');
return;
}
var hzIn = hzParsed.value;
var bwParsed = parseStackBlockWidthForSave(el('stack-block-width-tiles') ? el('stack-block-width-tiles').value : '');
if (!bwParsed.ok) {
setMsg('stack-game-msg', bwParsed.error, 'error');
return;
}
var blockW = bwParsed.value;
var btn = el('btn-stack-game-save');
stackGameSaveInFlight = true;
if (btn) {
btn.disabled = true;
if (!btn.dataset.prevLabel) btn.dataset.prevLabel = btn.textContent;
btn.textContent = 'กำลังบันทึก…';
}
gameTimingFetch('GET')
.then(function (data) {
data.stackSwingHz = hzIn;
data.stackBlockWidthTiles = blockW;
return gameTimingFetch('PUT', data);
})
.then(function () {
return gameTimingFetch('GET');
})
.then(function (fresh) {
applyGameTimingDataToStackInputs(fresh);
var hz = fresh && fresh.stackSwingHz != null ? Number(fresh.stackSwingHz) : hzIn;
if (!Number.isFinite(hz)) hz = hzIn;
hz = Math.round(hz * 100) / 100;
var wLabel = fresh && fresh.stackBlockWidthTiles != null && Number.isFinite(Number(fresh.stackBlockWidthTiles))
? String(Math.round(Number(fresh.stackBlockWidthTiles) * 100) / 100) + ' tile'
: 'อัตโนมัติ (จากแผนที่)';
setMsg('stack-game-msg', 'บันทึกสำเร็จ — สวิง ' + hz + ' Hz · กว้าง ' + wLabel + ' · โหลดยืนยันจากเซิร์ฟเวอร์แล้ว · ผู้เล่นต้องรีเฟรชหน้าเล่นหรือเข้าห้องใหม่', 'ok');
})
.catch(function (e) {
setMsg('stack-game-msg', (e.message || 'บันทึกไม่ได้') + ' — เช็คว่า Node รัน Game/server.js และ URL /Game/api/game-timing ไม่ 404', 'error');
})
.then(function () {
stackGameSaveInFlight = false;
if (btn) {
btn.disabled = false;
btn.textContent = btn.dataset.prevLabel || 'บันทึก';
}
});
}
function loadGameTimingPanel() {
gameTimingFetch('GET')
.then(function (data) {
el('game-timing-tick-ms').value = String(data.gauntletTickMs != null ? data.gauntletTickMs : 220);
el('game-timing-jump-ticks').value = String(data.gauntletJumpTicks != null ? data.gauntletJumpTicks : 16);
el('game-timing-limit-sec').value = String(data.gauntletTimeLimitSec != null ? data.gauntletTimeLimitSec : 0);
if (el('game-timing-lane-urls')) el('game-timing-lane-urls').value = laneUrlsToText(data.gauntletLaneImageUrls);
if (el('game-timing-laser-top-url')) el('game-timing-laser-top-url').value = data.gauntletLaserTopUrl != null ? String(data.gauntletLaserTopUrl) : '';
if (el('game-timing-laser-bottom-url')) el('game-timing-laser-bottom-url').value = data.gauntletLaserBottomUrl != null ? String(data.gauntletLaserBottomUrl) : '';
if (el('game-timing-laser-line-url')) el('game-timing-laser-line-url').value = data.gauntletLaserLineUrl != null ? String(data.gauntletLaserLineUrl) : '';
if (el('game-timing-laser-fill')) el('game-timing-laser-fill').value = data.gauntletLaserFillColor != null ? String(data.gauntletLaserFillColor) : '';
if (el('game-timing-laser-stroke')) el('game-timing-laser-stroke').value = data.gauntletLaserStrokeColor != null ? String(data.gauntletLaserStrokeColor) : '';
var lw = data.gauntletLaserLineWidthPx != null ? Number(data.gauntletLaserLineWidthPx) : 2;
if (el('game-timing-laser-line-width')) el('game-timing-laser-line-width').value = String(Number.isFinite(lw) ? lw : 2);
renderGameTimingLaneVisuals();
renderGameTimingLaserVisuals();
applyGameTimingDataToStackInputs(data);
setMsg('game-timing-msg', '', '');
return refreshGauntletAssetLibrary();
})
.catch(function (e) {
setMsg('game-timing-msg', e.message || 'โหลดไม่ได้', 'error');
});
}
function loadJumpSurviveTimingPanel() {
gameTimingFetch('GET')
.then(function (data) {
var inp = el('jump-survive-height-mult');
if (inp) {
var jsm = Number(data.jumpSurviveJumpHeightMult);
inp.value = String(Number.isFinite(jsm) ? Math.round(Math.max(0.5, Math.min(4, jsm)) * 100) / 100 : 1.5);
}
setMsg('jump-survive-timing-msg', '', '');
})
.catch(function (e) {
setMsg('jump-survive-timing-msg', e.message || 'โหลดไม่ได้', 'error');
});
}
function saveJumpSurviveTimingPanel() {
var inp = el('jump-survive-height-mult');
var jumpSurvMult = inp ? parseFloat(String(inp.value)) : 1.5;
if (Number.isNaN(jumpSurvMult)) jumpSurvMult = 1.5;
jumpSurvMult = Math.round(Math.max(0.5, Math.min(4, jumpSurvMult)) * 100) / 100;
gameTimingFetch('GET')
.then(function (data) {
data.jumpSurviveJumpHeightMult = jumpSurvMult;
return gameTimingFetch('PUT', data);
})
.then(function () {
setMsg('jump-survive-timing-msg', 'บันทึกแล้ว — ผู้เล่นกระโดดให้รอดได้ค่าใหม่เมื่อรีเฟรชหน้าเล่นหรือ sync จากเซิร์ฟเวอร์', 'ok');
})
.catch(function (e) {
setMsg('jump-survive-timing-msg', e.message || 'บันทึกไม่ได้', 'error');
});
}
function saveGameTimingPanel() {
var tickMs = parseInt(el('game-timing-tick-ms').value, 10);
var jumpT = parseInt(el('game-timing-jump-ticks').value, 10);
var limS = parseInt(el('game-timing-limit-sec').value, 10);
if (Number.isNaN(tickMs)) tickMs = 220;
if (Number.isNaN(jumpT)) jumpT = 16;
if (Number.isNaN(limS) || limS < 0) limS = 0;
tickMs = Math.max(80, Math.min(800, tickMs));
jumpT = Math.max(4, Math.min(40, jumpT));
limS = limS <= 0 ? 0 : Math.max(10, Math.min(7200, limS));
var lwPx = el('game-timing-laser-line-width') ? parseInt(el('game-timing-laser-line-width').value, 10) : 2;
if (Number.isNaN(lwPx)) lwPx = 2;
lwPx = Math.max(0, Math.min(24, lwPx));
/** แท็บเวลาเกม = เฉพาะ Gauntlet — อย่าส่ง stackSwingHz/stackBlockWidthTiles จากฟอร์ม (หลังรีเฟรชช่อง Stack ค้าง 0.55 จาก HTML จะทับค่าที่บันทึกไว้) */
gameTimingFetch('GET')
.then(function (data) {
data.gauntletTickMs = tickMs;
data.gauntletJumpTicks = jumpT;
data.gauntletTimeLimitSec = limS;
data.gauntletLaneImageUrls = parseLaneUrlsTextarea();
data.gauntletLaserTopUrl = el('game-timing-laser-top-url') ? String(el('game-timing-laser-top-url').value).trim() : '';
data.gauntletLaserBottomUrl = el('game-timing-laser-bottom-url') ? String(el('game-timing-laser-bottom-url').value).trim() : '';
data.gauntletLaserLineUrl = el('game-timing-laser-line-url') ? String(el('game-timing-laser-line-url').value).trim() : '';
data.gauntletLaserFillColor = el('game-timing-laser-fill') ? String(el('game-timing-laser-fill').value).trim() : '';
data.gauntletLaserStrokeColor = el('game-timing-laser-stroke') ? String(el('game-timing-laser-stroke').value).trim() : '';
data.gauntletLaserLineWidthPx = lwPx;
return gameTimingFetch('PUT', data);
})
.then(function () {
setMsg('game-timing-msg', 'บันทึกแล้ว — พรมแดง tick·กระโดด / รูปอุปสรรค์ · กระโดดให้รอดตั้งที่แท็บกระโดดให้รอด · ค่า Stack แก้ที่แท็บ Stack เท่านั้น · จำกัดเวลาใช้กับรอบถัดไปที่โฮสต์เริ่มเกม', 'ok');
})
.catch(function (e) {
setMsg('game-timing-msg', e.message || 'บันทึกไม่ได้', 'error');
});
}
function showPanel(name) {
['panel-setup', 'panel-login', 'panel-app'].forEach(function (id) {
var p = el(id);
if (p) p.hidden = id !== name;
});
el('top-actions').hidden = name !== 'panel-app';
}
function setTab(name) {
document.querySelectorAll('.tab').forEach(function (t) {
var on = t.getAttribute('data-tab') === name;
t.classList.toggle('is-active', on);
t.setAttribute('aria-selected', on ? 'true' : 'false');
});
document.querySelectorAll('.tab-panel').forEach(function (p) {
p.hidden = !p.id || p.id !== 'tab-panel-' + name;
});
var embedPanels = [
{ tab: 'map-editor', panel: 'tab-panel-map-editor', fsBtn: 'btn-map-editor-fullscreen' },
{ tab: 'characters', panel: 'tab-panel-characters', fsBtn: 'btn-character-fullscreen' },
];
embedPanels.forEach(function (ep) {
if (name === ep.tab) return;
var pnl = el(ep.panel);
var fsBtn = el(ep.fsBtn);
if (pnl && pnl.classList.contains('is-fullscreen')) {
pnl.classList.remove('is-fullscreen');
if (fsBtn) {
fsBtn.textContent = 'เต็มจอ';
fsBtn.setAttribute('aria-pressed', 'false');
}
}
});
if (name === 'quiz') loadQuizSettingsPanel();
if (name === 'quiz-carry') loadQuizCarryPanel();
if (name === 'quiz-battle') loadQbBattlePanel();
if (name === 'jump-survive') loadJumpSurviveTimingPanel();
if (name === 'game-timing') loadGameTimingPanel();
if (name === 'stack-game') loadStackGamePanel();
}
function boot() {
return api('session.php').then(function (s) {
if (s.needsSetup) {
showPanel('panel-setup');
return null;
}
if (!s.loggedIn) {
showPanel('panel-login');
return null;
}
showPanel('panel-app');
el('admin-user-label').textContent = (s.admin && s.admin.username) || '';
return s.admin;
});
}
el('form-setup').addEventListener('submit', function (e) {
e.preventDefault();
var fd = new FormData(e.target);
var p1 = fd.get('password');
var p2 = fd.get('password2');
if (p1 !== p2) {
setMsg('setup-msg', 'รหัสผ่านไม่ตรงกัน', 'error');
return;
}
api('setup.php', { method: 'POST', body: { password: p1 } })
.then(function () {
setMsg('setup-msg', 'สร้างสำเร็จ — กรุณาล็อกอิน', 'ok');
showPanel('panel-login');
})
.catch(function (err) {
setMsg('setup-msg', err.message || 'ผิดพลาด', 'error');
});
});
(function bindLoginPasswordToggle() {
var inp = el('login-password');
var btn = el('login-password-toggle');
if (!inp || !btn) return;
btn.addEventListener('click', function () {
var show = inp.type === 'password';
inp.type = show ? 'text' : 'password';
btn.setAttribute('aria-pressed', show ? 'true' : 'false');
btn.setAttribute('aria-label', show ? 'ซ่อนรหัสผ่าน' : 'แสดงรหัสผ่าน');
btn.textContent = show ? 'ซ่อน' : 'แสดง';
});
})();
el('form-login').addEventListener('submit', function (e) {
e.preventDefault();
var fd = new FormData(e.target);
api('login.php', {
method: 'POST',
body: {
username: fd.get('username'),
password: fd.get('password'),
},
})
.then(function () {
setMsg('login-msg', '', '');
return boot();
})
.then(function (admin) {
if (admin) loadAll(admin);
})
.catch(function (err) {
setMsg('login-msg', err.message || 'เข้าไม่ได้', 'error');
});
});
el('btn-logout').addEventListener('click', function () {
api('logout.php', { method: 'POST' })
.then(function () {
location.reload();
})
.catch(function () {
location.reload();
});
});
document.querySelectorAll('.tab').forEach(function (tab) {
tab.addEventListener('click', function () {
var name = tab.getAttribute('data-tab');
if (name === 'change-password' && tab.classList.contains('is-active')) {
setTab('oauth');
return;
}
setTab(name);
});
});
function bindEmbedPanelFullscreen(btnId, panelId) {
var btn = el(btnId);
var panel = el(panelId);
if (!btn || !panel) return;
function setFs(on) {
panel.classList.toggle('is-fullscreen', on);
btn.textContent = on ? 'ออกจากเต็มจอ' : 'เต็มจอ';
btn.setAttribute('aria-pressed', on ? 'true' : 'false');
}
btn.addEventListener('click', function () {
setFs(!panel.classList.contains('is-fullscreen'));
});
document.addEventListener('keydown', function (e) {
if (e.code !== 'Escape' || !panel.classList.contains('is-fullscreen')) return;
setFs(false);
});
}
bindEmbedPanelFullscreen('btn-map-editor-fullscreen', 'tab-panel-map-editor');
bindEmbedPanelFullscreen('btn-character-fullscreen', 'tab-panel-characters');
var btnGameTimingSave = el('btn-game-timing-save');
if (btnGameTimingSave) {
btnGameTimingSave.addEventListener('click', function () {
saveGameTimingPanel();
});
}
var btnJumpSurviveSave = el('btn-jump-survive-save');
if (btnJumpSurviveSave) {
btnJumpSurviveSave.addEventListener('click', saveJumpSurviveTimingPanel);
}
var btnStackGameSave = el('btn-stack-game-save');
if (btnStackGameSave) {
btnStackGameSave.addEventListener('click', saveStackGamePanel);
}
bindGauntletAssetLibrary();
bindGameTimingAssignedVisuals();
var btnQuizAdd = el('btn-quiz-admin-add');
var btnQuizSave = el('btn-quiz-admin-save');
if (btnQuizAdd) {
btnQuizAdd.addEventListener('click', function () {
addQuizAdminRow('', true);
});
}
if (btnQuizSave) btnQuizSave.addEventListener('click', saveQuizSettingsPanel);
var btnQuizCarryAdd = el('btn-quiz-carry-add');
var btnQuizCarrySave = el('btn-quiz-carry-save');
if (btnQuizCarryAdd) {
btnQuizCarryAdd.addEventListener('click', function () {
addQuizCarryAdminRow(null);
});
}
if (btnQuizCarrySave) btnQuizCarrySave.addEventListener('click', saveQuizCarryPanel);
wireQuizCarryPlaqueMapScaleControls();
quizCarryBindThemePickersOnce();
var btnQbBattleAdd = el('btn-qb-battle-add');
var btnQbBattleSave = el('btn-qb-battle-save');
if (btnQbBattleAdd) {
btnQbBattleAdd.addEventListener('click', function () {
addQbBattleRow(null);
refreshQbBattlePreview();
});
}
if (btnQbBattleSave) btnQbBattleSave.addEventListener('click', saveQbBattlePanel);
var qbFilterCat = el('qb-battle-filter-cat');
if (qbFilterCat) qbFilterCat.addEventListener('change', applyQbBattleFilter);
document.querySelectorAll('.qb-preview-mode-btn').forEach(function (b) {
b.addEventListener('click', function () {
document.querySelectorAll('.qb-preview-mode-btn').forEach(function (x) {
x.classList.toggle('is-active', x === b);
});
refreshQbBattlePreview();
});
});
function loadOAuth() {
return api('oauth.php').then(function (r) {
var o = r.oauth || {};
var f = el('form-oauth');
['facebookAppId', 'facebookAppSecret', 'facebookRedirectUri', 'googleClientId', 'googleClientSecret', 'googleRedirectUri'].forEach(function (k) {
var inp = f.querySelector('[name="' + k + '"]');
if (inp) inp.value = o[k] || '';
});
});
}
el('form-oauth').addEventListener('submit', function (e) {
e.preventDefault();
var fd = new FormData(e.target);
var body = {};
fd.forEach(function (v, k) {
body[k] = v;
});
api('oauth.php', { method: 'PUT', body: body })
.then(function () {
setMsg('oauth-msg', 'บันทึกแล้ว', 'ok');
})
.catch(function (err) {
setMsg('oauth-msg', err.message || 'บันทึกไม่สำเร็จ', 'error');
});
});
function renderAccounts(list) {
var tb = el('table-accounts').querySelector('tbody');
tb.innerHTML = '';
var sumCoins = 0;
(list || []).forEach(function (a) {
var c = Math.max(0, parseInt(a.coins, 10) || 0);
sumCoins += c;
var tr = document.createElement('tr');
tr.innerHTML =
'<td>' + escapeHtml(a.displayName || '—') + '</td>' +
'<td>' + escapeHtml(a.email || '—') + '</td>' +
'<td>' + escapeHtml(a.loginType || '') + '</td>' +
'<td><small>' + escapeHtml(a.providerUserId || '—') + '</small></td>' +
'<td class="td-coins">' +
'<input type="number" class="input-coins" min="0" step="1" value="' + c + '" data-account-id="' + escapeAttr(a.id) + '" aria-label="COINS">' +
'<button type="button" class="btn btn-primary btn-coins-save" data-id="' + escapeAttr(a.id) + '">บันทึก COINS</button>' +
'</td>' +
'<td>' + (a.blocked ? '<span class="msg error">บล็อก</span>' : 'ปกติ') + '</td>' +
'<td><button type="button" class="btn btn-danger btn-sm-del" data-id="' + escapeAttr(a.id) + '">ลบ</button> ' +
'<button type="button" class="btn btn-ghost btn-sm-toggle" data-id="' + escapeAttr(a.id) + '" data-blocked="' + (a.blocked ? '1' : '0') + '">' + (a.blocked ? 'ปลดบล็อก' : 'บล็อก') + '</button></td>';
tb.appendChild(tr);
});
var sumEl = el('accounts-coins-summary');
if (sumEl) {
sumEl.textContent = (list && list.length) ? ('ผู้ใช้ ' + list.length + ' บัญชี · รวม COINS ' + sumCoins) : 'ยังไม่มีบัญชี';
}
tb.querySelectorAll('.btn-sm-del').forEach(function (b) {
b.addEventListener('click', function () {
if (!confirm('ลบบัญชีนี้?')) return;
api('accounts.php?id=' + encodeURIComponent(b.getAttribute('data-id')), { method: 'DELETE' })
.then(loadAccounts)
.catch(function (err) {
setMsg('accounts-msg', err.message, 'error');
});
});
});
tb.querySelectorAll('.btn-sm-toggle').forEach(function (b) {
b.addEventListener('click', function () {
var id = b.getAttribute('data-id');
var blocked = b.getAttribute('data-blocked') === '1';
api('accounts.php', {
method: 'PATCH',
body: { id: id, blocked: !blocked },
})
.then(loadAccounts)
.catch(function (err) {
setMsg('accounts-msg', err.message, 'error');
});
});
});
tb.querySelectorAll('.btn-coins-save').forEach(function (b) {
b.addEventListener('click', function () {
var id = b.getAttribute('data-id');
var row = b.closest('tr');
var inp = row ? row.querySelector('input.input-coins') : null;
var coins = Math.max(0, parseInt(inp && inp.value, 10) || 0);
api('accounts.php', { method: 'PATCH', body: { id: id, coins: coins } })
.then(function () {
setMsg('accounts-msg', 'อัปเดต COINS แล้ว', 'ok');
return loadAccounts();
})
.catch(function (err) {
setMsg('accounts-msg', err.message, 'error');
});
});
});
}
function escapeHtml(s) {
var d = document.createElement('div');
d.textContent = s == null ? '' : String(s);
return d.innerHTML;
}
function escapeAttr(s) {
return String(s || '').replace(/"/g, '&quot;');
}
function loadAccounts() {
return api('accounts.php').then(function (r) {
renderAccounts(r.accounts);
setMsg('accounts-msg', '', '');
});
}
el('form-account-add').addEventListener('submit', function (e) {
e.preventDefault();
var fd = new FormData(e.target);
var body = {
email: fd.get('email'),
displayName: fd.get('displayName'),
loginType: fd.get('loginType'),
providerUserId: fd.get('providerUserId'),
notes: fd.get('notes'),
blocked: fd.get('blocked') === 'on',
coins: Math.max(0, parseInt(fd.get('coins'), 10) || 0),
};
api('accounts.php', { method: 'POST', body: body })
.then(function () {
e.target.reset();
return loadAccounts();
})
.catch(function (err) {
setMsg('accounts-msg', err.message, 'error');
});
});
function renderAdmins(list, myId) {
var tb = el('table-admins').querySelector('tbody');
tb.innerHTML = '';
(list || []).forEach(function (a) {
var tr = document.createElement('tr');
var delBtn = a.id === myId
? '—'
: '<button type="button" class="btn btn-danger btn-adm-del" data-id="' + escapeAttr(a.id) + '">ลบ</button>';
tr.innerHTML =
'<td>' + escapeHtml(a.username) + '</td>' +
'<td>' + escapeHtml(a.role || 'admin') + '</td>' +
'<td><small>' + escapeHtml((a.createdAt || '').slice(0, 10)) + '</small></td>' +
'<td>' + delBtn + '</td>';
tb.appendChild(tr);
});
tb.querySelectorAll('.btn-adm-del').forEach(function (b) {
b.addEventListener('click', function () {
if (!confirm('ลบแอดมินนี้?')) return;
api('admins.php?id=' + encodeURIComponent(b.getAttribute('data-id')), { method: 'DELETE' })
.then(loadAdmins)
.catch(function (err) {
setMsg('admins-msg', err.message, 'error');
});
});
});
}
function fillAdminResetSelect(admins, myId) {
var sel = el('select-admin-reset-target');
if (!sel) return;
sel.innerHTML = '<option value="">— เลือก —</option>';
(admins || []).forEach(function (a) {
if (!a.id || a.id === myId) return;
var opt = document.createElement('option');
opt.value = a.id;
opt.textContent = a.username + ' (' + (a.role || 'admin') + ')';
sel.appendChild(opt);
});
}
function loadAdmins() {
return api('admins.php').then(function (r) {
return api('session.php').then(function (s) {
var myId = s.admin && s.admin.id;
renderAdmins(r.admins, myId);
fillAdminResetSelect(r.admins, myId);
setMsg('admins-msg', '', '');
});
});
}
el('form-change-password').addEventListener('submit', function (e) {
e.preventDefault();
var fd = new FormData(e.target);
var cur = fd.get('currentPassword');
var n1 = fd.get('newPassword');
var n2 = fd.get('newPassword2');
if (n1 !== n2) {
setMsg('password-self-msg', 'รหัสใหม่ไม่ตรงกัน', 'error');
return;
}
api('password.php', {
method: 'POST',
body: { currentPassword: cur, newPassword: n1 },
})
.then(function () {
e.target.reset();
setMsg('password-self-msg', 'เปลี่ยนรหัสผ่านแล้ว', 'ok');
})
.catch(function (err) {
setMsg('password-self-msg', err.message, 'error');
});
});
el('form-admin-reset-other').addEventListener('submit', function (e) {
e.preventDefault();
var fd = new FormData(e.target);
var tid = fd.get('targetId');
var n1 = fd.get('newPassword');
var n2 = fd.get('newPassword2');
if (!tid) {
setMsg('admins-msg', 'เลือกแอดมิน', 'error');
return;
}
if (n1 !== n2) {
setMsg('admins-msg', 'รหัสใหม่ไม่ตรงกัน', 'error');
return;
}
api('admins.php', {
method: 'PATCH',
body: { id: tid, newPassword: n1 },
})
.then(function () {
e.target.reset();
setMsg('admins-msg', 'ตั้งรหัสใหม่ให้แอดมินที่เลือกแล้ว', 'ok');
})
.catch(function (err) {
setMsg('admins-msg', err.message, 'error');
});
});
el('form-admin-add').addEventListener('submit', function (e) {
e.preventDefault();
var fd = new FormData(e.target);
api('admins.php', {
method: 'POST',
body: {
username: fd.get('username'),
password: fd.get('password'),
role: fd.get('role'),
},
})
.then(function () {
e.target.reset();
return loadAdmins();
})
.catch(function (err) {
setMsg('admins-msg', err.message, 'error');
});
});
function getDesiredAdminTab(admin) {
var want = null;
try {
want = new URL(window.location.href).searchParams.get('tab');
} catch (e) { /* ignore */ }
var allowed = { 'change-password': true, oauth: true, 'map-editor': true, quiz: true, 'quiz-carry': true, 'quiz-battle': true, 'jump-survive': true, 'game-timing': true, 'stack-game': true, characters: true, accounts: true, admins: true };
if (!want || !allowed[want]) return 'oauth';
if (want === 'admins' && (!admin || admin.role !== 'super')) return 'oauth';
return want;
}
function loadAll(admin) {
loadOAuth().catch(function (e) {
setMsg('oauth-msg', e.message, 'error');
});
loadAccounts().catch(function (e) {
setMsg('accounts-msg', e.message, 'error');
});
var isSuper = admin && admin.role === 'super';
el('tab-admins').hidden = !isSuper;
el('form-admin-add').hidden = !isSuper;
if (isSuper) {
loadAdmins().catch(function (e) {
setMsg('admins-msg', e.message, 'error');
});
}
setTab(getDesiredAdminTab(admin));
/** อย่า prefetch ซิงก์ Stack ที่นี่ — GET ที่เริ่มก่อน PUT บันทึกอาจกลับช้ากว่าและทับช่องกว้าง/HZ ด้วยข้อมูลเก่า · โหลดจริงที่แท็บ Stack / เวลาเกมแทน */
}
boot().then(function (admin) {
if (admin) loadAll(admin);
});
})();