diff --git a/www/html/Admin/admin.css b/www/html/Admin/admin.css index 5ec7d1c..355bd35 100644 --- a/www/html/Admin/admin.css +++ b/www/html/Admin/admin.css @@ -572,6 +572,182 @@ legend { color: var(--accent); } +/* —— Tab group dividers (จัดหมวดเมนูให้สแกนง่าย) —— */ +.admin-tab-sep { + flex: 1 1 100%; + display: flex; + align-items: center; + gap: 0.55rem; + margin: 0.35rem 0.15rem 0; + padding: 0 0.15rem; + user-select: none; +} +.admin-tab-sep:first-child { + margin-top: 0; +} +.admin-tab-sep span { + font-size: 0.66rem; + font-weight: 700; + letter-spacing: 0.09em; + text-transform: uppercase; + color: var(--muted); + white-space: nowrap; +} +.admin-tab-sep::after { + content: ''; + flex: 1; + height: 1px; + background: linear-gradient(90deg, var(--border), transparent); +} +@media (max-width: 899px) { + /* มือถือ: แถบเลื่อนแนวนอน — ไม่ต้องแสดงหัวข้อกลุ่ม */ + .admin-tab-sep { display: none; } +} + +/* —— ชุดคำถามพิเศษ (Special Quiz panel) —— */ +.sq-title { margin: 0 0 0.35rem; } +.sq-steps { + list-style: none; + display: flex; + flex-wrap: wrap; + gap: 0.6rem; + margin: 0.9rem 0 1.1rem; + padding: 0; +} +.sq-steps li { + display: flex; + align-items: center; + gap: 0.5rem; + flex: 1 1 220px; + padding: 0.55rem 0.8rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: rgba(255, 255, 255, 0.02); + font-size: 0.88rem; +} +.sq-step-no { + flex: 0 0 auto; + width: 1.65rem; + height: 1.65rem; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 50%; + background: var(--accent-dim); + color: var(--accent); + font-weight: 700; + font-size: 0.9rem; +} +.sq-fieldset { + margin: 0 0 1.1rem; + padding: 0.95rem 1.1rem 1.1rem; + border: 1px solid var(--border); + border-radius: var(--radius-lg); + background: var(--bg-elevated, rgba(255, 255, 255, 0.02)); +} +.sq-fieldset > legend { + font-size: 0.95rem; + font-weight: 700; + padding: 0 0.45rem; + color: var(--text); +} +.sq-pick-row { + display: flex; + flex-wrap: wrap; + gap: 1rem; + align-items: flex-end; +} +.sq-select { min-width: 200px; } +.sq-set-tag { + margin-left: auto; + align-self: center; + font-size: 0.8rem; + font-weight: 600; + color: var(--accent); + background: var(--accent-dim); + padding: 0.3rem 0.7rem; + border-radius: 999px; + white-space: nowrap; +} +.sq-card-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 0.85rem; +} +.sq-card-grid .sq-card-url { grid-column: 1 / -1; } +.sq-card-grid input, +.sq-card-grid select { width: 100%; } +.sq-count { + font-weight: 400; + font-size: 0.8rem; + color: var(--muted); + margin-left: 0.3rem; +} + +/* —— คำถามตัวเลือก A/B/C/D (Special Quiz MCQ rows) —— */ +.sq-mcq-row { + display: flex; + flex-direction: column; + gap: 0.7rem; + padding: 0.85rem 0.95rem; + margin-bottom: 0.85rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: rgba(255, 255, 255, 0.02); +} +.sq-mcq-q { + display: flex; + flex-direction: column; + gap: 0.3rem; + font-size: 0.82rem; + font-weight: 600; + color: var(--muted); +} +.sq-mcq-text { width: 100%; } +.sq-mcq-choices { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 0.55rem; +} +.sq-mcq-choice { + display: flex; + align-items: center; + gap: 0.5rem; +} +.sq-mcq-letter { + flex: 0 0 auto; + width: 1.7rem; + height: 1.7rem; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 8px; + background: var(--accent-dim); + color: var(--accent); + font-weight: 700; + font-size: 0.85rem; +} +.sq-mcq-choice-input { flex: 1 1 auto; min-width: 0; } +.sq-mcq-foot { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + flex-wrap: wrap; +} +.sq-mcq-ans { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.82rem; + font-weight: 600; + color: var(--text); +} +.sq-mcq-correct { min-width: 5rem; } +@media (max-width: 620px) { + .sq-mcq-choices { grid-template-columns: 1fr; } +} + .tab-panel[hidden] { display: none !important; } diff --git a/www/html/Admin/admin.js b/www/html/Admin/admin.js index dc4455c..715309a 100644 --- a/www/html/Admin/admin.js +++ b/www/html/Admin/admin.js @@ -246,6 +246,189 @@ }); } + /* ===== ชุดคำถามพิเศษ (Level×Case = 15 ชุด) รับการ์ดพิเศษ ===== */ + var SPECIAL_CARD_RARITIES = ['common', 'rare', 'legendary']; + var specialQuizSetsState = {}; + var specialQuizCurrentKey = 'L1_C1'; + + function specialQuizSelectorKey() { + var L = el('special-quiz-level'); + var C = el('special-quiz-case'); + return 'L' + ((L && L.value) || '1') + '_C' + ((C && C.value) || '1'); + } + + function updateSpecialQuizCount() { + var c = el('special-quiz-count'); + if (!c) return; + var n = document.querySelectorAll('#special-quiz-questions-list .quiz-admin-q-row').length; + c.textContent = '(' + n + ' ข้อ)'; + } + + var SPECIAL_QUIZ_CHOICE_COUNT = 4; + var SPECIAL_QUIZ_LETTERS = ['A', 'B', 'C', 'D', 'E', 'F']; + + /** คำถามตัวเลือก A/B/C/D — เลือกคำตอบถูก 1 ข้อ */ + function addSpecialQuizRow(q) { + q = q || {}; + var list = el('special-quiz-questions-list'); + if (!list) return; + var choices = Array.isArray(q.choices) ? q.choices : []; + var correctIndex = Number(q.correctIndex); + if (!(correctIndex >= 0 && correctIndex < SPECIAL_QUIZ_CHOICE_COUNT)) correctIndex = 0; + + var row = document.createElement('div'); + row.className = 'quiz-admin-q-row sq-mcq-row'; + + var qLab = document.createElement('label'); + qLab.className = 'sq-mcq-q'; + qLab.appendChild(document.createTextNode('คำถาม')); + var qInp = document.createElement('input'); + qInp.type = 'text'; + qInp.className = 'sq-mcq-text'; + qInp.maxLength = 500; + qInp.placeholder = 'พิมพ์คำถาม...'; + qInp.value = q.text || ''; + qLab.appendChild(qInp); + + var grid = document.createElement('div'); + grid.className = 'sq-mcq-choices'; + for (var i = 0; i < SPECIAL_QUIZ_CHOICE_COUNT; i++) { + var cl = document.createElement('label'); + cl.className = 'sq-mcq-choice'; + var badge = document.createElement('span'); + badge.className = 'sq-mcq-letter'; + badge.textContent = SPECIAL_QUIZ_LETTERS[i]; + var ci = document.createElement('input'); + ci.type = 'text'; + ci.className = 'sq-mcq-choice-input'; + ci.maxLength = 200; + ci.placeholder = 'ตัวเลือก ' + SPECIAL_QUIZ_LETTERS[i]; + ci.value = choices[i] || ''; + ci.setAttribute('data-choice-index', String(i)); + cl.appendChild(badge); + cl.appendChild(ci); + grid.appendChild(cl); + } + + var foot = document.createElement('div'); + foot.className = 'sq-mcq-foot'; + var ansLab = document.createElement('label'); + ansLab.className = 'sq-mcq-ans'; + ansLab.appendChild(document.createTextNode('คำตอบที่ถูก ')); + var ansSel = document.createElement('select'); + ansSel.className = 'sq-mcq-correct'; + for (var j = 0; j < SPECIAL_QUIZ_CHOICE_COUNT; j++) { + var o = document.createElement('option'); + o.value = String(j); + o.textContent = SPECIAL_QUIZ_LETTERS[j]; + if (j === correctIndex) o.selected = true; + ansSel.appendChild(o); + } + ansLab.appendChild(ansSel); + + 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(); + updateSpecialQuizCount(); + }); + foot.appendChild(ansLab); + foot.appendChild(rm); + + row.appendChild(qLab); + row.appendChild(grid); + row.appendChild(foot); + list.appendChild(row); + } + + function renderSpecialQuizSet(key) { + var set = specialQuizSetsState[key] || { questions: [], specialCard: {} }; + var card = (set.specialCard && typeof set.specialCard === 'object') ? set.specialCard : {}; + if (el('special-card-th')) el('special-card-th').value = card.th || ''; + if (el('special-card-en')) el('special-card-en').value = card.en || ''; + if (el('special-card-rarity')) { + el('special-card-rarity').value = SPECIAL_CARD_RARITIES.indexOf(card.rarity) >= 0 ? card.rarity : 'rare'; + } + if (el('special-card-image')) el('special-card-image').value = card.imageUrl || ''; + var list = el('special-quiz-questions-list'); + if (list) { + list.innerHTML = ''; + var qs = (set.questions && set.questions.length) + ? set.questions + : [{ type: 'mcq', text: '', choices: ['', '', '', ''], correctIndex: 0 }]; + qs.forEach(addSpecialQuizRow); + } + var tag = el('special-quiz-set-tag'); + if (tag) tag.textContent = 'ชุดที่แก้: ' + key; + updateSpecialQuizCount(); + } + + function captureSpecialQuizForm(key) { + var questions = []; + document.querySelectorAll('#special-quiz-questions-list .sq-mcq-row').forEach(function (row) { + var inp = row.querySelector('.sq-mcq-text'); + var t = inp && inp.value ? String(inp.value).trim() : ''; + if (!t) return; + var choices = []; + row.querySelectorAll('.sq-mcq-choice-input').forEach(function (ci) { + choices.push(String(ci.value || '').trim()); + }); + if (choices.filter(function (c) { return c; }).length < 2) return; + var sel = row.querySelector('.sq-mcq-correct'); + var correctIndex = sel ? parseInt(sel.value, 10) : 0; + if (!(correctIndex >= 0 && correctIndex < choices.length) || !choices[correctIndex]) { + correctIndex = choices.findIndex(function (c) { return c; }); + if (correctIndex < 0) correctIndex = 0; + } + questions.push({ type: 'mcq', text: t, choices: choices, correctIndex: correctIndex }); + }); + var rar = el('special-card-rarity') ? el('special-card-rarity').value : 'rare'; + specialQuizSetsState[key] = { + questions: questions, + specialCard: { + th: el('special-card-th') ? String(el('special-card-th').value || '').trim() : '', + en: el('special-card-en') ? String(el('special-card-en').value || '').trim() : '', + rarity: SPECIAL_CARD_RARITIES.indexOf(rar) >= 0 ? rar : 'rare', + imageUrl: el('special-card-image') ? String(el('special-card-image').value || '').trim() : '', + }, + }; + } + + function onSpecialQuizSelectorChange() { + captureSpecialQuizForm(specialQuizCurrentKey); + specialQuizCurrentKey = specialQuizSelectorKey(); + renderSpecialQuizSet(specialQuizCurrentKey); + } + + function loadSpecialQuizPanel() { + setMsg('special-quiz-msg', 'กำลังโหลด...', ''); + gameQuizFetch('GET').then(function (data) { + specialQuizSetsState = (data.specialQuizSets && typeof data.specialQuizSets === 'object') ? data.specialQuizSets : {}; + specialQuizCurrentKey = specialQuizSelectorKey(); + renderSpecialQuizSet(specialQuizCurrentKey); + setMsg('special-quiz-msg', '', ''); + }).catch(function (e) { + setMsg('special-quiz-msg', e.message || 'โหลดไม่ได้', 'error'); + }); + } + + function saveSpecialQuizPanel() { + captureSpecialQuizForm(specialQuizCurrentKey); + setMsg('special-quiz-msg', 'กำลังบันทึก...', ''); + gameQuizFetch('PUT', { specialQuizSets: specialQuizSetsState }).then(function (res) { + if (res && res.specialQuizSets && typeof res.specialQuizSets === 'object') { + specialQuizSetsState = res.specialQuizSets; + } + setMsg('special-quiz-msg', 'บันทึกแล้ว (15 ชุด)', 'ok'); + renderSpecialQuizSet(specialQuizCurrentKey); + }).catch(function (e) { + setMsg('special-quiz-msg', e.message || 'บันทึกไม่ได้', 'error'); + }); + } + function quizTfThemeHiddenId(kind) { if (kind === 'bg') return 'quiz-tf-theme-bg'; if (kind === 'border') return 'quiz-tf-theme-border'; @@ -3740,6 +3923,7 @@ } }); if (name === 'quiz') loadQuizSettingsPanel(); + if (name === 'special-quiz') loadSpecialQuizPanel(); if (name === 'quiz-carry') loadQuizCarryPanel(); if (name === 'quiz-battle') loadQbBattlePanel(); if (name === 'jump-survive') loadJumpSurviveTimingPanel(); @@ -3935,6 +4119,20 @@ } if (btnQuizSave) btnQuizSave.addEventListener('click', saveQuizSettingsPanel); + var btnSpecialQuizAdd = el('btn-special-quiz-add'); + var btnSpecialQuizSave = el('btn-special-quiz-save'); + if (btnSpecialQuizAdd) { + btnSpecialQuizAdd.addEventListener('click', function () { + addSpecialQuizRow({ type: 'tf', text: '', answerTrue: true }); + updateSpecialQuizCount(); + }); + } + if (btnSpecialQuizSave) btnSpecialQuizSave.addEventListener('click', saveSpecialQuizPanel); + var specialQuizLevelSel = el('special-quiz-level'); + var specialQuizCaseSel = el('special-quiz-case'); + if (specialQuizLevelSel) specialQuizLevelSel.addEventListener('change', onSpecialQuizSelectorChange); + if (specialQuizCaseSel) specialQuizCaseSel.addEventListener('change', onSpecialQuizSelectorChange); + var btnQuizCarryAdd = el('btn-quiz-carry-add'); var btnQuizCarrySave = el('btn-quiz-carry-save'); if (btnQuizCarryAdd) { @@ -4228,7 +4426,7 @@ 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, 'space-shooter': true, 'mega-virus': true, 'game-timing': true, 'stack-game': true, characters: true, accounts: true, admins: true }; + var allowed = { 'change-password': true, oauth: true, 'map-editor': true, quiz: true, 'special-quiz': true, 'quiz-carry': true, 'quiz-battle': true, 'jump-survive': true, 'space-shooter': true, 'mega-virus': 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; diff --git a/www/html/Admin/api/game-quiz-settings.php b/www/html/Admin/api/game-quiz-settings.php index e5541f1..ffa98fb 100644 --- a/www/html/Admin/api/game-quiz-settings.php +++ b/www/html/Admin/api/game-quiz-settings.php @@ -52,7 +52,7 @@ function merge_quiz_settings_disk_from_put(string $rawBody): bool if ($hadFileButInvalidJson) { return false; } - $mergeKeys = ['readMs', 'answerMs', 'betweenMs', 'carryReadMs', 'carryAnswerMs', 'carrySessionLength', 'carryMapPanelTheme', 'quizMapPanelTheme', 'carryEmbedCountdownTheme', 'carryChoicePlaqueTheme', 'carryChoicePlaqueThemes', 'carryChoicePlaqueMapScale', 'carryWalkSpeedMultForMapId', 'carryWalkSpeedMult', 'quizRoundQuestionCount', 'questions', 'carryQuestions', 'battleQuizMcq']; + $mergeKeys = ['readMs', 'answerMs', 'betweenMs', 'carryReadMs', 'carryAnswerMs', 'carrySessionLength', 'carryMapPanelTheme', 'quizMapPanelTheme', 'carryEmbedCountdownTheme', 'carryChoicePlaqueTheme', 'carryChoicePlaqueThemes', 'carryChoicePlaqueMapScale', 'carryWalkSpeedMultForMapId', 'carryWalkSpeedMult', 'quizRoundQuestionCount', 'questions', 'carryQuestions', 'battleQuizMcq', 'specialQuizSets']; foreach ($mergeKeys as $k) { if (array_key_exists($k, $patch)) { $base[$k] = $patch[$k]; @@ -96,6 +96,9 @@ function overlay_quiz_settings_from_disk(string $nodeBody): string if (array_key_exists('battleQuizMcq', $disk)) { $j['battleQuizMcq'] = $disk['battleQuizMcq']; } + if (array_key_exists('specialQuizSets', $disk) && is_array($disk['specialQuizSets'])) { + $j['specialQuizSets'] = $disk['specialQuizSets']; + } if (array_key_exists('carryMapPanelTheme', $disk) && is_array($disk['carryMapPanelTheme'])) { $j['carryMapPanelTheme'] = $disk['carryMapPanelTheme']; } diff --git a/www/html/Admin/index.html b/www/html/Admin/index.html index d94bbed..6478626 100644 --- a/www/html/Admin/index.html +++ b/www/html/Admin/index.html @@ -10,7 +10,7 @@ - +
ข้ามไปเนื้อหา @@ -66,9 +66,12 @@