diff --git a/.validate_dashboard_items_syntax.js b/.validate_dashboard_items_syntax.js new file mode 100644 index 0000000..a57bb2c --- /dev/null +++ b/.validate_dashboard_items_syntax.js @@ -0,0 +1,84 @@ +const fs = require('fs'); +const path = 'src/server/views/partials/dashboard/dashboard_items.ejs'; +try{ + const s = fs.readFileSync(path, 'utf8'); + const m = s.match(/]*>([\s\S]*?)<\/script>/i); + if (!m) { console.error('NO_SCRIPT'); process.exit(2); } + let script = m[1]; + // replace EJS output tags with empty string literal and remove other EJS tags + script = script.replace(/<%=([\s\S]*?)%>/g, "''"); + script = script.replace(/<%[\s\S]*?%>/g, ''); + try { + // quick balance scanner to find likely unclosed tokens + (function scan() { + const s2 = script; + const stack = []; + let inSingle = false, inDouble = false, inTpl = false; + let inLineComment = false, inBlockComment = false; + for (let i = 0; i < s2.length; i++) { + const ch = s2[i]; + const prev = s2[i-1]; + // comments handling + if (!inSingle && !inDouble && !inTpl) { + if (!inBlockComment && ch === '/' && s2[i+1] === '/') { inLineComment = true; continue; } + if (!inLineComment && ch === '/' && s2[i+1] === '*') { inBlockComment = true; i++; continue; } + } + if (inLineComment) { if (ch === '\n') inLineComment = false; continue; } + if (inBlockComment) { if (ch === '*' && s2[i+1] === '/') { inBlockComment = false; i++; } continue; } + // string toggles + if (!inDouble && !inTpl && ch === '\'' && prev !== '\\') { inSingle = !inSingle; continue; } + if (!inSingle && !inTpl && ch === '"' && prev !== '\\') { inDouble = !inDouble; continue; } + if (!inSingle && !inDouble && ch === '`' && prev !== '\\') { inTpl = !inTpl; continue; } + if (inSingle || inDouble || inTpl) continue; + // brackets + if (ch === '(' || ch === '{' || ch === '[') stack.push({ ch, pos: i }); + if (ch === ')' || ch === '}' || ch === ']') { + const last = stack.pop(); + if (!last) { console.error('UNMATCHED_CLOSE', ch, 'at', i); break; } + const map = { '(':')','{':'}','[':']' }; + if (map[last.ch] !== ch) { console.error('MISMATCH', last.ch, 'opened at', last.pos, 'but closed by', ch, 'at', i); break; } + } + } + if (inSingle || inDouble || inTpl) console.error('UNTERMINATED_STRING_OR_TEMPLATE'); + if (inBlockComment) console.error('UNTERMINATED_BLOCK_COMMENT'); + if (stack.length) { + const last = stack[stack.length-1]; + // compute line/col + const upTo = s2.slice(0, last.pos); + const line = upTo.split('\n').length; + const col = last.pos - upTo.lastIndexOf('\n'); + console.error('UNMATCHED_OPEN', last.ch, 'at index', last.pos, 'line', line, 'col', col); + const context = s2.slice(Math.max(0, last.pos-40), Math.min(s2.length, last.pos+40)).replace(/\n/g, '\\n'); + console.error('CONTEXT:', context); + } + })(); + + // try acorn parse first for better diagnostics (if available) + try { + const acorn = require('acorn'); + acorn.parse(script, { ecmaVersion: 2020 }); + } catch (eac) { + console.error('ACORN_PARSE_ERROR:' + (eac && eac.message ? eac.message : String(eac))); + if (eac && eac.loc) console.error('loc', eac.loc); + // fallthrough to vm.Script for older Node versions + } + const vm = require('vm'); + new vm.Script(script, { filename: 'dashboard_items_script.js' }); + console.log('OK'); + process.exit(0); + } catch (e) { + console.error('SYNTAX_ERROR:' + (e && e.message ? e.message : String(e))); + if (e && e.stack) console.error(e.stack); + if (e && typeof e.lineNumber !== 'undefined') console.error('line:' + e.lineNumber + ' col:' + (e.columnNumber||'?')); + // print first 200 lines for inspection + const lines = script.split('\n'); + for (let i = 0; i < Math.min(lines.length, 400); i++) { + const n = (i+1).toString().padStart(4, ' '); + console.error(n + ': ' + lines[i]); + } + process.exit(3); + } +} catch (e) { + console.error('READ_ERROR:' + e.message); + process.exit(4); +} diff --git a/package-lock.json b/package-lock.json index 3645576..0eeea32 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@google/genai": "1.20.0", "@google/generative-ai": "0.24.1", "@prisma/client": "6.16.2", + "acorn": "8.15.0", "appwrite": "20.1.0", "chrono-node": "2.9.0", "discord-api-types": "0.38.26", diff --git a/package.json b/package.json index 22349c9..32bad62 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@google/genai": "1.20.0", "@google/generative-ai": "0.24.1", "@prisma/client": "6.16.2", + "acorn": "8.15.0", "appwrite": "20.1.0", "chrono-node": "2.9.0", "discord-api-types": "0.38.26", diff --git a/src/server/public/assets/js/dashboard_items.js b/src/server/public/assets/js/dashboard_items.js new file mode 100644 index 0000000..095e617 --- /dev/null +++ b/src/server/public/assets/js/dashboard_items.js @@ -0,0 +1,212 @@ +(function(){ + // read guildId from DOM data- attribute (keeps this script EJS-free) + const guildId = (() => { try{ return (document.getElementById('itemsRoot') && document.getElementById('itemsRoot').dataset && document.getElementById('itemsRoot').dataset.guildId) || ''; }catch(e){ return ''; } })(); + + const $ = id => document.getElementById(id); + const _escapeMap = {'&':'&','<':'<','>':'>','"':'"',"'":"'"}; + const escapeHtml = s => { if (s==null) return ''; return String(s).replace(/[&<>"']/g, ch => _escapeMap[ch]); }; + + function showPageAlert(type, msg, ttl){ + const c = $('pageAlert'); if(!c) return; + c.classList.remove('hidden'); c.innerHTML = ''; + const wrapper = document.createElement('div'); + const color = (type === 'success') ? 'bg-green-600' : (type === 'danger') ? 'bg-red-700' : (type === 'warning') ? 'bg-amber-700' : 'bg-sky-600'; + wrapper.className = ['p-3','rounded',color,'text-white','flex','items-center','justify-between'].join(' '); + const txt = document.createElement('div'); txt.textContent = msg || ''; + const btn = document.createElement('button'); btn.id = 'pageAlertClose'; btn.className = 'ml-4 font-bold'; btn.textContent = '✕'; + btn.addEventListener('click', ()=>{ c.classList.add('hidden'); c.innerHTML = ''; }); + wrapper.appendChild(txt); wrapper.appendChild(btn); c.appendChild(wrapper); + if (ttl) setTimeout(()=>{ c.classList.add('hidden'); c.innerHTML = ''; }, ttl); + } + function clearPageAlert(){ const c = $('pageAlert'); if(c){ c.classList.add('hidden'); c.innerHTML=''; } } + + function setModalError(msg){ const e = $('modalError'); if(!e) return; e.textContent = msg||''; e.classList.toggle('hidden', !msg); } + function clearModalError(){ setModalError(''); } + + let cachedItems = []; + const list = $('itemsList'); + + async function fetchItems(){ + if(!guildId) return; + if(list) list.textContent = 'Cargando items...'; + try{ + const res = await fetch('/api/dashboard/' + encodeURIComponent(guildId) + '/items', { headers:{ 'Accept':'application/json' } }); + if(!res.ok) throw new Error('fetch-failed'); + const j = await res.json(); if(!j || !j.ok) throw new Error('bad'); + cachedItems = j.items || []; + renderList(cachedItems); + }catch(err){ if(list) list.innerHTML = '
Error cargando items
'; } + } + + function renderList(items){ + if(!list) return; list.innerHTML = ''; + if(!Array.isArray(items) || items.length===0){ list.innerHTML = '
No hay items definidos.
'; return; } + items.forEach(it => { + const card = document.createElement('div'); card.className = 'p-3 bg-[#071a2a] rounded flex items-start justify-between'; + const left = document.createElement('div'); + // build left column using DOM to avoid inline HTML/template complexity + const titleDiv = document.createElement('div'); + titleDiv.className = 'text-white font-medium'; + titleDiv.textContent = (it.name || '') + ' (' + (it.key || '') + ')'; + const descDiv = document.createElement('div'); + descDiv.className = 'text-white/60 text-sm mt-1'; + descDiv.textContent = it.description || ''; + left.appendChild(titleDiv); + left.appendChild(descDiv); + const right = document.createElement('div'); + right.className = 'flex items-center gap-2'; + const editBtn = document.createElement('button'); + editBtn.className = 'editBtn px-2 py-1 bg-indigo-600 rounded text-white text-sm'; + editBtn.textContent = 'Editar'; + editBtn.dataset.id = it.id; + const delBtn = document.createElement('button'); + delBtn.className = 'delBtn px-2 py-1 bg-red-600 rounded text-white text-sm'; + delBtn.textContent = 'Eliminar'; + delBtn.dataset.id = it.id; + right.appendChild(editBtn); right.appendChild(delBtn); + card.appendChild(left); card.appendChild(right); list.appendChild(card); + }); + Array.from(list.querySelectorAll('.editBtn')).forEach(b=>b.addEventListener('click', onEdit)); + Array.from(list.querySelectorAll('.delBtn')).forEach(b=>b.addEventListener('click', onDelete)); + } + + // rewards helpers + function getCurrentRewards(){ try{ const r = $('rewardsList'); return r && r.dataset && r.dataset.rewards ? JSON.parse(r.dataset.rewards) : []; }catch(e){ return []; } } + function setCurrentRewards(arr){ const r = $('rewardsList'); if(!r) return; r.dataset.rewards = JSON.stringify(arr||[]); } + function renderRewardsList(arr){ const container = $('rewardsList'); if(!container) return; container.innerHTML = ''; if(!Array.isArray(arr) || arr.length===0) return; arr.forEach((it,idx)=>{ const row = document.createElement('div'); row.className='flex items-center gap-2'; if(it.coins || it.type==='coins'){ row.innerHTML = '
Coins: ' + escapeHtml(String(it.coins||it.amount||0)) + '
'; } else if(it.items || it.itemKey){ const key = it.itemKey || (it.items && it.items[0] && it.items[0].key) || ''; const qty = it.quantity || (it.items && it.items[0] && it.items[0].quantity) || 1; row.innerHTML = '
Item: ' + escapeHtml(key) + ' x' + escapeHtml(String(qty)) + '
'; } else { row.innerHTML = '
' + escapeHtml(JSON.stringify(it)) + '
'; } const del = document.createElement('button'); del.className='px-2 py-1 bg-red-600 text-white rounded text-sm ml-2'; del.textContent='Eliminar'; del.addEventListener('click', ()=>{ const cur = getCurrentRewards(); cur.splice(idx,1); setCurrentRewards(cur); renderRewardsList(cur); }); row.appendChild(del); container.appendChild(row); }); } + + // small handlers + const addRewardBtn = $('addRewardBtn'); if(addRewardBtn) addRewardBtn.addEventListener('click', ()=>{ + clearModalError(); const type = $('newRewardType') ? $('newRewardType').value : 'items'; const amtRaw = $('newRewardAmount') ? $('newRewardAmount').value : ''; const amt = Number(amtRaw); const key = $('newRewardItemKey') ? $('newRewardItemKey').value.trim() : ''; const cur = getCurrentRewards(); + if(type==='coins'){ if(!amtRaw || isNaN(amt) || amt<=0){ setModalError('Cantidad de coins debe ser mayor a 0'); return; } cur.push({ coins: amt }); } + else { if(!key){ setModalError('item.key requerido'); return; } if(!amtRaw || isNaN(amt) || amt<=0){ setModalError('Cantidad de item debe ser mayor a 0'); return; } cur.push({ items:[{ key, quantity: amt||1 }] }); } + setCurrentRewards(cur); renderRewardsList(cur); if($('newRewardAmount')) $('newRewardAmount').value=''; if($('newRewardItemKey')) $('newRewardItemKey').value=''; clearModalError(); + }); + const newRewardType = $('newRewardType'); if(newRewardType) newRewardType.addEventListener('change', ()=>{ const k = $('newRewardItemKey'); if(!k) return; if(newRewardType.value==='coins'){ k.disabled = true; k.classList.add('opacity-50'); } else { k.disabled = false; k.classList.remove('opacity-50'); } }); + + async function tryLoadRawProps(id){ const msg = $('loadRawMsg'); if(!id) return null; if(msg) msg.textContent = 'cargando...'; try{ const res = await fetch('/api/dashboard/' + encodeURIComponent(guildId) + '/items/' + encodeURIComponent(id) + '/raw', { headers:{ 'Accept':'application/json' } }); if(!res.ok){ if(msg) msg.textContent = 'error'; return null; } const j = await res.json(); if(!j || !j.ok){ if(msg) msg.textContent = 'no data'; return null; } if(msg) msg.textContent = 'raw cargado'; return j.item || null; }catch(e){ if(msg) msg.textContent = 'error'; return null; } } + + async function onEdit(e){ clearModalError(); const id = e.currentTarget && e.currentTarget.dataset ? e.currentTarget.dataset.id : null; if(!id) return; let item = (cachedItems.find(x=>String(x.id)===String(id))||null); + if(!item){ try{ const res = await fetch('/api/dashboard/' + encodeURIComponent(guildId) + '/items/' + encodeURIComponent(id)); if(!res.ok) throw new Error('fetch'); const j = await res.json(); if(j && j.ok) { item = j.item; cachedItems.push(item); } }catch(err){ setModalError('Error cargando item'); return; } } + if(!item) return setModalError('Item no encontrado'); + // map fields + if($('itemId')) $('itemId').value = item.id || ''; + if($('fieldKey')) $('fieldKey').value = item.key || ''; + if($('fieldName')) $('fieldName').value = item.name || ''; + if($('fieldCategory')) $('fieldCategory').value = item.category || ''; + if($('fieldIcon')) $('fieldIcon').value = item.icon || ''; + if($('fieldDescription')) $('fieldDescription').value = item.description || ''; + if($('fieldTags')) $('fieldTags').value = Array.isArray(item.tags)?item.tags.join(','):item.tags||''; + if($('fieldMaxPer')) $('fieldMaxPer').value = item.maxPerInventory||''; + const p = item.props || {}; + try{ + if($('propCraftable')) $('propCraftable').checked = !!p.craftable; + if($('propRecipeKey')) $('propRecipeKey').value = p.recipe && p.recipe.key ? p.recipe.key : ''; + if($('propEquipable')) $('propEquipable').checked = !!p.equipable; + if($('propSlot')) $('propSlot').value = p.slot || ''; + if($('propAttack')) $('propAttack').value = p.attack || ''; + if($('propDefense')) $('propDefense').value = p.defense || ''; + if($('propDurability')) $('propDurability').value = p.durability || ''; + if($('propMaxDurability')) $('propMaxDurability').value = p.maxDurability || ''; + if($('propToolType')) $('propToolType').value = (p.tool && p.tool.type) ? p.tool.type : ''; + if($('propToolTier')) $('propToolTier').value = (p.tool && typeof p.tool.tier !== 'undefined') ? String(p.tool.tier) : ''; + if($('propBreakable')) $('propBreakable').checked = !!(p.breakable && p.breakable.enabled); + if($('propDurabilityPerUse')) $('propDurabilityPerUse').value = (p.breakable && p.breakable.durabilityPerUse) ? String(p.breakable.durabilityPerUse) : ''; + if($('propUsable')) $('propUsable').checked = !!p.usable; + if($('propPurgeAllEffects')) $('propPurgeAllEffects').checked = !!p.purgeAllEffects; + if($('propHealAmount')) $('propHealAmount').value = p.heal || ''; + if($('propDamage')) $('propDamage').value = p.damage || ''; + if($('propDamageBonus')) $('propDamageBonus').value = p.damageBonus || ''; + if($('statAttack')) $('statAttack').value = p.stats && typeof p.stats.attack !== 'undefined' ? String(p.stats.attack) : ''; + if($('statHp')) $('statHp').value = p.stats && typeof p.stats.hp !== 'undefined' ? String(p.stats.hp) : ''; + if($('statDefense')) $('statDefense').value = p.stats && typeof p.stats.defense !== 'undefined' ? String(p.stats.defense) : ''; + if($('statXpReward')) $('statXpReward').value = p.stats && typeof p.stats.xpReward !== 'undefined' ? String(p.stats.xpReward) : ''; + if($('reqToolRequired')) $('reqToolRequired').checked = !!(p.requirements && p.requirements.tool && p.requirements.tool.required); + if($('reqToolType')) $('reqToolType').value = (p.requirements && p.requirements.tool && p.requirements.tool.toolType) ? p.requirements.tool.toolType : ''; + if($('reqMinTier')) $('reqMinTier').value = (p.requirements && p.requirements.tool && typeof p.requirements.tool.minTier !== 'undefined') ? String(p.requirements.tool.minTier) : ''; + if($('propSellPrice')) $('propSellPrice').value = p.sellPrice || ''; + if($('propBuyPrice')) $('propBuyPrice').value = p.buyPrice || ''; + if($('propChestEnabled')) $('propChestEnabled').checked = !!(p.chest && p.chest.enabled); + const rewards = p.chest && p.chest.rewards ? p.chest.rewards : []; + setCurrentRewards(rewards||[]); renderRewardsList(rewards||[]); + if($('propCustomJson')){ const copy = Object.assign({}, p); ['craftable','recipe','equipable','slot','attack','defense','durability','maxDurability'].forEach(k=>delete copy[k]); $('propCustomJson').value = JSON.stringify(copy, null, 2); } + }catch(e){} + const m = item.metadata || {}; + if($('metaRarity')) $('metaRarity').value = m.rarity || 'common'; + if($('metaWeight')) $('metaWeight').value = typeof m.weight !== 'undefined' ? String(m.weight) : ''; + if($('metaCustomJson')){ const copyM = Object.assign({}, m); delete copyM.rarity; delete copyM.weight; $('metaCustomJson').value = JSON.stringify(copyM, null, 2); } + renderTagChips(); + if($('modalTitle')) $('modalTitle').textContent = 'Editar item'; + if($('itemModal')){ $('itemModal').classList.remove('hidden'); $('itemModal').classList.add('flex'); } + } + + async function onDelete(e){ const id = e.currentTarget && e.currentTarget.dataset ? e.currentTarget.dataset.id : null; if(!id) return; if(!confirm('Eliminar item?')) return; try{ const res = await fetch('/api/dashboard/' + encodeURIComponent(guildId) + '/items/' + encodeURIComponent(id), { method:'DELETE' }); if(!res.ok) throw new Error('delete-failed'); await fetchItems(); showPageAlert('success','Item eliminado',3000); }catch(err){ showPageAlert('danger','Error al eliminar'); } } + + // create/cancel + const createBtn = $('createItemBtn'); if(createBtn) createBtn.addEventListener('click', ()=>{ clearModalError(); resetFormForCreate(); if($('modalTitle')) $('modalTitle').textContent = 'Crear item'; if($('itemModal')){ $('itemModal').classList.remove('hidden'); $('itemModal').classList.add('flex'); } }); + const cancelBtn = $('cancelItemBtn'); if(cancelBtn) cancelBtn.addEventListener('click', ()=>{ clearModalError(); if($('itemModal')){ $('itemModal').classList.add('hidden'); $('itemModal').classList.remove('flex'); } }); + + function resetFormForCreate(){ try{ const ids = ['itemId','fieldKey','fieldName','fieldCategory','fieldIcon','fieldDescription','fieldTags','fieldMaxPer']; ids.forEach(id=>{ const e=$(id); if(e) e.value=''; }); const checks = ['propCraftable','propEquipable','propBreakable','propUsable','propPurgeAllEffects','propChestEnabled','reqToolRequired']; checks.forEach(id=>{ const e=$(id); if(e && e.type==='checkbox') e.checked=false; }); if($('propCustomJson')) $('propCustomJson').value='{}'; if($('metaCustomJson')) $('metaCustomJson').value='{}'; setCurrentRewards([]); renderRewardsList([]); renderTagChips(); }catch(e){} + } + + // tag chips + const tagsChips = document.createElement('div'); tagsChips.id='tagsChips'; tagsChips.className='mt-2 flex flex-wrap gap-2'; if($('fieldTags') && $('fieldTags').parentNode) $('fieldTags').parentNode.appendChild(tagsChips); + function renderTagChips(){ try{ const base = $('fieldTags') && $('fieldTags').value ? $('fieldTags').value.split(',').map(s=>s.trim()).filter(Boolean) : []; const auto=[]; if($('propCraftable') && $('propCraftable').checked) auto.push('craftable'); if($('propToolType') && $('propToolType').value.trim()) auto.push('tool'); if(($('propDamage') && $('propDamage').value.trim())||($('propAttack') && $('propAttack').value.trim())) auto.push('weapon'); if($('propUsable') && $('propUsable').checked) auto.push('consumable'); if($('propEquipable') && $('propEquipable').checked) auto.push('equipable'); if($('propChestEnabled') && $('propChestEnabled').checked) auto.push('chest'); if($('propBreakable') && $('propBreakable').checked) auto.push('breakable'); if($('propSellPrice') && $('propSellPrice').value.trim()) auto.push('sellable'); if($('propBuyPrice') && $('propBuyPrice').value.trim()) auto.push('buyable'); const merged = Array.from(new Set([...(base||[]), ...auto])); tagsChips.innerHTML=''; merged.forEach(t=>{ const chip = document.createElement('span'); chip.className='px-2 py-1 rounded bg-white/6 text-sm text-white'; chip.textContent = t; tagsChips.appendChild(chip); }); }catch(e){} + ['propCraftable','propToolType','propDamage','propAttack','propUsable','propEquipable','propChestEnabled','propBreakable','propSellPrice','propBuyPrice','fieldTags'].forEach(id=>{ const e=$(id); if(!e) return; e.addEventListener('input', renderTagChips); e.addEventListener('change', renderTagChips); }); + + // form submit + const form = $('itemForm'); if(form) form.addEventListener('submit', async ev=>{ + ev.preventDefault(); clearModalError(); try{ + const id = $('itemId') ? $('itemId').value : ''; + const parsedProps = {}; + if($('propCraftable') && $('propCraftable').checked){ parsedProps.craftable = true; const rk = $('propRecipeKey') ? $('propRecipeKey').value.trim() : ''; if(rk) parsedProps.recipe = { key: rk }; } + if($('propEquipable') && $('propEquipable').checked){ parsedProps.equipable = true; const slot = $('propSlot') ? $('propSlot').value : ''; if(slot) parsedProps.slot = slot; } + const ttype = $('propToolType') ? $('propToolType').value.trim() : ''; const ttier = $('propToolTier') ? $('propToolTier').value.trim() : ''; if(ttype) parsedProps.tool = Object.assign({}, parsedProps.tool||{}, { type: ttype }); if(ttier) parsedProps.tool = Object.assign({}, parsedProps.tool||{}, { tier: Number(ttier) }); + if($('propBreakable') && $('propBreakable').checked){ parsedProps.breakable = parsedProps.breakable||{}; parsedProps.breakable.enabled = true; const dpu = $('propDurabilityPerUse') ? $('propDurabilityPerUse').value.trim() : ''; if(dpu) parsedProps.breakable.durabilityPerUse = Number(dpu); } + const attack = $('propAttack') ? $('propAttack').value.trim() : ''; if(attack) parsedProps.attack = Number(attack); + const defense = $('propDefense') ? $('propDefense').value.trim() : ''; if(defense) parsedProps.defense = Number(defense); + const durability = $('propDurability') ? $('propDurability').value.trim() : ''; if(durability) parsedProps.durability = Number(durability); + const maxDur = $('propMaxDurability') ? $('propMaxDurability').value.trim() : ''; if(maxDur) parsedProps.maxDurability = Number(maxDur); + if($('propUsable') && $('propUsable').checked){ parsedProps.usable = true; if($('propPurgeAllEffects') && $('propPurgeAllEffects').checked) parsedProps.purgeAllEffects = true; const heal = $('propHealAmount') ? $('propHealAmount').value.trim() : ''; if(heal) parsedProps.heal = Number(heal); } + const dmg = $('propDamage') ? $('propDamage').value.trim() : ''; if(dmg) parsedProps.damage = Number(dmg); + const dmgBonus = $('propDamageBonus') ? $('propDamageBonus').value.trim() : ''; if(dmgBonus) parsedProps.damageBonus = Number(dmgBonus); + const sAtk = $('statAttack') ? $('statAttack').value.trim() : ''; if(sAtk) { parsedProps.stats = parsedProps.stats||{}; parsedProps.stats.attack = Number(sAtk); } + const sHp = $('statHp') ? $('statHp').value.trim() : ''; if(sHp) { parsedProps.stats = parsedProps.stats||{}; parsedProps.stats.hp = Number(sHp); } + const sDef = $('statDefense') ? $('statDefense').value.trim() : ''; if(sDef) { parsedProps.stats = parsedProps.stats||{}; parsedProps.stats.defense = Number(sDef); } + const sXp = $('statXpReward') ? $('statXpReward').value.trim() : ''; if(sXp) { parsedProps.stats = parsedProps.stats||{}; parsedProps.stats.xpReward = Number(sXp); } + if($('reqToolRequired') && $('reqToolRequired').checked){ parsedProps.requirements = parsedProps.requirements||{}; parsedProps.requirements.tool = { required: true }; const rtype = $('reqToolType') ? $('reqToolType').value.trim() : ''; if(rtype) parsedProps.requirements.tool.toolType = rtype; const rmin = $('reqMinTier') ? $('reqMinTier').value.trim() : ''; if(rmin) parsedProps.requirements.tool.minTier = Number(rmin); } + const sell = $('propSellPrice') ? $('propSellPrice').value.trim() : ''; if(sell) parsedProps.sellPrice = Number(sell); + const buy = $('propBuyPrice') ? $('propBuyPrice').value.trim() : ''; if(buy) parsedProps.buyPrice = Number(buy); + if($('propChestEnabled') && $('propChestEnabled').checked){ parsedProps.chest = parsedProps.chest||{}; parsedProps.chest.enabled = true; } + try{ const rawRewards = $('rewardsList') ? $('rewardsList').dataset.rewards : null; const rewards = rawRewards ? JSON.parse(rawRewards) : null; if(Array.isArray(rewards)) parsedProps.chest = Object.assign(parsedProps.chest||{}, { rewards }); }catch(e){ setModalError('JSON inválido en Drops/Chest: ' + (e && e.message ? e.message : String(e))); return; } + try{ const custom = $('propCustomJson') ? JSON.parse($('propCustomJson').value || '{}') : {}; if(custom && typeof custom === 'object') Object.assign(parsedProps, custom); }catch(e){ setModalError('JSON inválido en Props personalizado: ' + (e && e.message ? e.message : String(e))); return; } + + const parsedMeta = { rarity: ($('metaRarity') ? $('metaRarity').value : 'common') }; + const w = $('metaWeight') ? $('metaWeight').value.trim() : ''; if(w) parsedMeta.weight = Number(w); + try{ const customM = $('metaCustomJson') ? JSON.parse($('metaCustomJson').value || '{}') : {}; if(customM && typeof customM === 'object') Object.assign(parsedMeta, customM); }catch(e){ setModalError('JSON inválido en Metadata personalizado: ' + (e && e.message ? e.message : String(e))); return; } + + const payload = { + key: ($('fieldKey') ? $('fieldKey').value.trim() : ''), + name: ($('fieldName') ? $('fieldName').value.trim() : ''), + category: ($('fieldCategory') ? $('fieldCategory').value.trim() : ''), + icon: ($('fieldIcon') ? $('fieldIcon').value.trim() : ''), + description: ($('fieldDescription') ? $('fieldDescription').value.trim() : ''), + tags: ($('fieldTags') ? $('fieldTags').value.split(',').map(s=>s.trim()).filter(Boolean) : []), + maxPerInventory: ($('fieldMaxPer') ? Number($('fieldMaxPer').value) || null : null), + props: Object.keys(parsedProps).length ? parsedProps : null, + metadata: Object.keys(parsedMeta).length ? parsedMeta : null, + }; + try{ const auto = new Set(payload.tags || []); if(parsedProps.craftable) auto.add('craftable'); if(parsedProps.tool) auto.add('tool'); if(parsedProps.damage||parsedProps.attack||parsedProps.stats) auto.add('weapon'); if(parsedProps.usable) auto.add('consumable'); if(parsedProps.equipable) auto.add('equipable'); if(parsedProps.chest) auto.add('chest'); if(parsedProps.breakable) auto.add('breakable'); if(parsedProps.sellPrice) auto.add('sellable'); if(parsedProps.buyPrice) auto.add('buyable'); payload.tags = Array.from(auto).filter(Boolean); }catch(e){} + + try{ + if(id){ const res = await fetch('/api/dashboard/' + encodeURIComponent(guildId) + '/items/' + encodeURIComponent(id), { method:'PUT', headers:{ 'Content-Type':'application/json' }, body: JSON.stringify(payload) }); if(!res.ok) throw new Error('save-failed'); } + else { const res = await fetch('/api/dashboard/' + encodeURIComponent(guildId) + '/items', { method:'POST', headers:{ 'Content-Type':'application/json' }, body: JSON.stringify(payload) }); if(!res.ok) throw new Error('create-failed'); } + if($('itemModal')){ $('itemModal').classList.add('hidden'); $('itemModal').classList.remove('flex'); } + clearModalError(); await fetchItems(); + }catch(e){ setModalError('Error al guardar item. ¿JSON válido en Props/Metadata?'); } + }catch(e){ setModalError('Error interno: ' + (e && e.message ? e.message : String(e))); } + }); + + // initial + fetchItems(); +})(); diff --git a/src/server/views/partials/dashboard/dashboard_items.ejs b/src/server/views/partials/dashboard/dashboard_items.ejs index 41566ac..40ce645 100644 --- a/src/server/views/partials/dashboard/dashboard_items.ejs +++ b/src/server/views/partials/dashboard/dashboard_items.ejs @@ -1,4 +1,4 @@ -
+

Items

@@ -7,774 +7,5 @@
- -
- <% if (typeof guildRoles !== 'undefined') { %> - - <% } %> -
Cargando items...
-
- - - - - +
diff --git a/src/server/views/partials/dashboard/dashboard_settings.ejs b/src/server/views/partials/dashboard/dashboard_settings.ejs index 43d501e..ee16f4a 100644 --- a/src/server/views/partials/dashboard/dashboard_settings.ejs +++ b/src/server/views/partials/dashboard/dashboard_settings.ejs @@ -1,4 +1,4 @@ -
+

Ajustes del servidor

diff --git a/tmp_acorn.js b/tmp_acorn.js new file mode 100644 index 0000000..8201e53 --- /dev/null +++ b/tmp_acorn.js @@ -0,0 +1,20 @@ +const fs = require('fs'); const acorn = require('acorn'); +const path = '/home/shni/amayo/amayo/src/server/views/partials/dashboard/dashboard_items.ejs'; +const s = fs.readFileSync(path,'utf8'); +const m = s.match(/]*>([\s\S]*?)<\/script>/i); +if(!m){ console.error('NO_SCRIPT'); process.exit(2);} const src = m[1]; +try{ + acorn.parse(src, {ecmaVersion:2020}); + console.log('ACORN OK'); +}catch(err){ + console.error('ACORN ERR', err.message); + if(err.loc){ + const lines = src.split('\n'); + const L = err.loc.line; const C = err.loc.column; + console.error('at line', L, 'col', C); + const start = Math.max(0, L-4); const end = Math.min(lines.length, L+2); + for(let i=start;i' : ' ') + n.toString().padStart(4,' ') + '| ' + lines[i]); + } + } +} diff --git a/tmp_check.js b/tmp_check.js new file mode 100644 index 0000000..90a07f4 --- /dev/null +++ b/tmp_check.js @@ -0,0 +1,43 @@ +const fs = require('fs'); +const path = '/home/shni/amayo/amayo/src/server/views/partials/dashboard/dashboard_items.ejs'; +const s = fs.readFileSync(path,'utf8'); +const m = s.match(/]*>([\s\S]*?)<\/script>/i); +if(!m){ console.error('NO_SCRIPT'); process.exit(2); } +const src = m[1]; +console.log('SCRIPT LENGTH:', src.length); +let stack = []; +const opens = {'(':')','[':']','{':'}'}; +const closes = {')':'(',']':'[','}':'{'}; +let inSingle=false, inDouble=false, inTemplate=false, inComment=false, inLineComment=false, escape=false; +let lastSingle=-1, lastDouble=-1, lastTemplate=-1; +for(let i=0;i', src.slice(Math.max(0,lastSingle-60), lastSingle+60)); + if(inDouble) console.error('lastDouble@', lastDouble, 'context=>', src.slice(Math.max(0,lastDouble-60), lastDouble+60)); + if(inTemplate) console.error('lastTemplate@', lastTemplate, 'context=>', src.slice(Math.max(0,lastTemplate-60), lastTemplate+60)); +} +if(stack.length) console.error('UNMATCHED_OPEN', stack[stack.length-1], 'context=>', src.slice(Math.max(0,stack[stack.length-1].i-40), stack[stack.length-1].i+40)); +console.log('DONE'); +process.exit(0); diff --git a/tmp_find_parse_error.js b/tmp_find_parse_error.js new file mode 100644 index 0000000..ac5e056 --- /dev/null +++ b/tmp_find_parse_error.js @@ -0,0 +1,11 @@ +const fs=require('fs'); const acorn=require('acorn'); +const path='/home/shni/amayo/amayo/src/server/views/partials/dashboard/dashboard_items.ejs'; +const s=fs.readFileSync(path,'utf8'); const m=s.match(/]*>([\s\S]*?)<\/script>/i); if(!m){ console.error('NO_SCRIPT'); process.exit(2);} const src=m[1]; +let low=0, high=src.length, bad=-1; +while(low]*>([\s\S]*?)<\/script>/i); if(!m){ console.error('NO_SCRIPT'); process.exit(2);} const src=m[1]; +const lines=src.split('\n'); +for(let i=1;i<=lines.length;i++){ + const chunk = lines.slice(0,i).join('\n'); + try{ acorn.parse(chunk,{ecmaVersion:2020}); } + catch(err){ console.error('FAIL at line', i, 'message', err.message); console.error('Error loc', err.loc); console.error('Context:'); const start=Math.max(1,i-5); const end=Math.min(lines.length, i+2); for(let j=start;j<=end;j++){ console.error((j===i? '>' : ' ')+j.toString().padStart(4,' ')+'| '+lines[j-1]); } process.exit(1); } +} +console.log('ALL LINES PARSED OK'); diff --git a/tmp_print_script.js b/tmp_print_script.js new file mode 100644 index 0000000..58ad6f4 --- /dev/null +++ b/tmp_print_script.js @@ -0,0 +1,11 @@ +const fs = require('fs'); +const path = '/home/shni/amayo/amayo/src/server/views/partials/dashboard/dashboard_items.ejs'; +const s = fs.readFileSync(path,'utf8'); +const m = s.match(/]*>([\s\S]*?)<\/script>/i); +if(!m){ console.error('NO_SCRIPT'); process.exit(2); } +const src = m[1].replace(/\r\n/g,'\n'); +const lines = src.split('\n'); +for(let i=0;i]*>([\s\S]*?)<\/script>/i); if(!m){ console.error('NO_SCRIPT'); process.exit(2);} const src=m[1]; +const tok = acorn.tokenizer(src, {ecmaVersion:2020}); +let token; +const stack=[]; +while((token=tok.getToken()).type.label!="eof"){ + const lb = token.type.label; + if(lb==='(' || lb==='[' || lb==='{') stack.push({ch:lb, pos: token.start}); + if(lb===')' || lb===']' || lb==='}'){ + const expected = (lb===')'? '(' : (lb===']'? '[' : '{')); + if(stack.length===0){ console.error('UNMATCHED_CLOSE', lb, 'at', token.start); process.exit(3); } + const top = stack.pop(); + if(top.ch !== expected){ console.error('MISMATCH', top, 'vs', lb, 'at', token.start); process.exit(4); } + } +} +if(stack.length) console.error('UNMATCHED_OPEN', stack[stack.length-1]); else console.log('BALANCED'); diff --git a/tmp_token_trace.js b/tmp_token_trace.js new file mode 100644 index 0000000..f4c9c0b --- /dev/null +++ b/tmp_token_trace.js @@ -0,0 +1,20 @@ +const fs=require('fs'); const acorn=require('acorn'); +const path='/home/shni/amayo/amayo/src/server/views/partials/dashboard/dashboard_items.ejs'; +const s=fs.readFileSync(path,'utf8'); const m=s.match(/]*>([\s\S]*?)<\/script>/i); if(!m){ console.error('NO_SCRIPT'); process.exit(2);} const src=m[1]; +const tok = acorn.tokenizer(src, {ecmaVersion:2020}); +let t; const stack=[]; +while((t=tok.getToken()).type.label!=='eof'){ + const lb = t.type.label; + if(lb==='(' || lb==='[' || lb==='{'){ + stack.push({ch:lb,pos:t.start}); + if(t.start>2600 && t.start<3100) console.log('PUSH', lb, 'pos', t.start, 'stacklen', stack.length); + } + if(lb===')' || lb===']' || lb==='}'){ + const expected = (lb===')'? '(' : (lb===']'? '[' : '{')); + if(t.start>2600 && t.start<3100) console.log('POP', lb, 'pos', t.start, 'expect', expected, 'stacklen(before)', stack.length); + if(stack.length===0){ console.error('UNMATCHED_CLOSE', lb, 'at', t.start); process.exit(3); } + const top = stack.pop(); + if(top.ch !== expected){ console.error('MISMATCH', top, 'vs', lb, 'at', t.start); process.exit(4); } + } +} +console.log('FINISHED, stacklen', stack.length); diff --git a/tmp_tokens_inspect.js b/tmp_tokens_inspect.js new file mode 100644 index 0000000..63702db --- /dev/null +++ b/tmp_tokens_inspect.js @@ -0,0 +1,8 @@ +const fs=require('fs'); const acorn=require('acorn'); +const path='/home/shni/amayo/amayo/src/server/views/partials/dashboard/dashboard_items.ejs'; +const s=fs.readFileSync(path,'utf8'); const m=s.match(/]*>([\s\S]*?)<\/script>/i); const src=m[1]; +const tok = acorn.tokenizer(src,{ecmaVersion:2020}); +let t; while((t=tok.getToken()).type.label!=='eof'){ + if(t.start>=2600 && t.start<=3050){ console.log(t.start, t.end, t.type.label, t.value); } +} +console.log('done');