feat(dashboard): enhance dashboard settings UI and add item management scripts

- Updated the dashboard settings partial to include a semi-transparent background for better visibility.
- Introduced a new script for validating the syntax of dashboard item scripts, ensuring better error handling and diagnostics.
- Added a comprehensive JavaScript file for managing dashboard items, including fetching, rendering, editing, and deleting items.
- Implemented various utility functions for handling alerts, rewards, and modal interactions within the item management interface.
- Created temporary scripts for debugging and parsing errors in item scripts, aiding in development and maintenance.
This commit is contained in:
Shni
2025-10-15 15:53:50 -05:00
parent 818dbba654
commit 408ac0e3fb
14 changed files with 441 additions and 772 deletions

View File

@@ -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(/<script[^>]*>([\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);
}

1
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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 = {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":"&#39;"};
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 = '<div class="text-red-400">Error cargando items</div>'; }
}
function renderList(items){
if(!list) return; list.innerHTML = '';
if(!Array.isArray(items) || items.length===0){ list.innerHTML = '<div class="text-white/60">No hay items definidos.</div>'; 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 = '<div class="text-white/80">Coins: <strong class="text-white">' + escapeHtml(String(it.coins||it.amount||0)) + '</strong></div>'; } 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 = '<div class="text-white/80">Item: <strong class="text-white">' + escapeHtml(key) + '</strong> x' + escapeHtml(String(qty)) + '</div>'; } else { row.innerHTML = '<div class="text-white/80">' + escapeHtml(JSON.stringify(it)) + '</div>'; } 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();
})();

View File

@@ -1,4 +1,4 @@
<div class="p-4 bg-[#071323] rounded">
<div id="itemsRoot" data-guild-id="<%= selectedGuildId ? selectedGuildId : '' %>" class="p-4 bg-gray-800/50 rounded">
<div class="flex items-center justify-between mb-4">
<h2 class="text-white text-lg font-semibold">Items</h2>
<div>
@@ -7,774 +7,5 @@
</div>
<div id="pageAlert" class="hidden mb-3"></div>
<div id="itemsList" class="space-y-3">
<% if (typeof guildRoles !== 'undefined') { %>
<!-- placeholder: server will not inject items; client will fetch -->
<% } %>
<div class="text-white/60">Cargando items...</div>
</div>
<!-- Create / Edit modal -->
<div id="itemModal" class="fixed inset-0 hidden items-center justify-center z-50">
<div class="absolute inset-0 bg-black/70"></div>
<div class="relative w-full max-w-3xl bg-[#0b1320] rounded shadow p-6 z-10">
<h3 id="modalTitle" class="text-white text-lg mb-2">Crear item</h3>
<div id="modalError" class="hidden text-red-400 text-sm mb-2"></div>
<form id="itemForm">
<input type="hidden" name="id" id="itemId">
<div class="grid grid-cols-2 gap-3">
<div>
<label class="text-white/80">Key</label>
<input id="fieldKey" name="key" class="w-full mt-1 p-2 rounded bg-[#0f1720] text-white" />
</div>
<div>
<label class="text-white/80">Name</label>
<input id="fieldName" name="name" class="w-full mt-1 p-2 rounded bg-[#0f1720] text-white" />
</div>
<div>
<label class="text-white/80">Category</label>
<input id="fieldCategory" name="category" class="w-full mt-1 p-2 rounded bg-[#0f1720] text-white" />
</div>
<div>
<label class="text-white/80">Icon</label>
<input id="fieldIcon" name="icon" class="w-full mt-1 p-2 rounded bg-[#0f1720] text-white" />
</div>
<div class="col-span-2">
<label class="text-white/80">Description</label>
<textarea id="fieldDescription" name="description" class="w-full mt-1 p-2 rounded bg-[#0f1720] text-white" rows="3"></textarea>
</div>
<div>
<label class="text-white/80">Tags (comma separated)</label>
<input id="fieldTags" name="tags" class="w-full mt-1 p-2 rounded bg-[#0f1720] text-white" />
</div>
<div>
<label class="text-white/80">Max Per Inventory</label>
<input id="fieldMaxPer" name="maxPerInventory" type="number" class="w-full mt-1 p-2 rounded bg-[#0f1720] text-white" />
</div>
<div class="col-span-2">
<label class="text-white/80">Props (UI)</label>
<div id="propsUI" class="w-full mt-1 p-3 rounded bg-[#0f1720] text-white space-y-2">
<div class="flex items-center gap-3">
<input type="checkbox" id="propCraftable" />
<label for="propCraftable" class="text-white/80">Craftable</label>
<input id="propRecipeKey" placeholder="recipe.key (opcional)" class="ml-3 p-1 rounded bg-[#0f1620] text-white text-sm" />
</div>
<div class="flex items-center gap-3">
<input type="checkbox" id="propEquipable" />
<label for="propEquipable" class="text-white/80">Equipable</label>
<label class="text-white/80 ml-3">Slot</label>
<select id="propSlot" class="p-1 rounded bg-[#0f1620] text-white text-sm">
<option value="">--</option>
<option value="head">head</option>
<option value="body">body</option>
<option value="legs">legs</option>
<option value="hand">hand</option>
</select>
</div>
<div class="flex items-center gap-3">
<label class="text-white/80">Attack</label>
<input id="propAttack" type="number" min="0" class="p-1 rounded bg-[#0f1620] text-white text-sm w-24" />
<label class="text-white/80 ml-3">Defense</label>
<input id="propDefense" type="number" min="0" class="p-1 rounded bg-[#0f1620] text-white text-sm w-24" />
</div>
<div class="flex items-center gap-3">
<label class="text-white/80">Durability</label>
<input id="propDurability" type="number" min="0" class="p-1 rounded bg-[#0f1620] text-white text-sm w-24" />
<label class="text-white/80 ml-3">Max Durability</label>
<input id="propMaxDurability" type="number" min="0" class="p-1 rounded bg-[#0f1620] text-white text-sm w-24" />
</div>
<div class="flex items-center gap-3">
<label class="text-white/80">Custom JSON (for advanced)</label>
<textarea id="propCustomJson" class="w-full mt-1 p-2 rounded bg-[#0f1720] text-white font-mono" rows="3">{}</textarea>
</div>
<div class="border-t border-white/6 pt-2">
<h4 class="text-white/80 text-sm mb-2">Tool</h4>
<div class="flex items-center gap-3">
<label class="text-white/80">Type</label>
<input id="propToolType" class="p-1 rounded bg-[#0f1620] text-white text-sm" placeholder="pickaxe/sword/etc" />
<label class="text-white/80 ml-3">Tier</label>
<input id="propToolTier" type="number" min="0" class="p-1 rounded bg-[#0f1620] text-white text-sm w-24" />
</div>
</div>
<div class="border-t border-white/6 pt-2">
<h4 class="text-white/80 text-sm mb-2">Breakable / Durability</h4>
<div class="flex items-center gap-3">
<input type="checkbox" id="propBreakable" />
<label for="propBreakable" class="text-white/80">Breakable</label>
<label class="text-white/80 ml-3">Durability per use</label>
<input id="propDurabilityPerUse" type="number" min="0" class="p-1 rounded bg-[#0f1620] text-white text-sm w-24" />
</div>
</div>
<div class="border-t border-white/6 pt-2">
<h4 class="text-white/80 text-sm mb-2">Consumable / Usable</h4>
<div class="flex items-center gap-3">
<input type="checkbox" id="propUsable" />
<label for="propUsable" class="text-white/80">Usable</label>
<input type="checkbox" id="propPurgeAllEffects" />
<label for="propPurgeAllEffects" class="text-white/80">Purge all effects</label>
</div>
<div class="flex items-center gap-3 mt-2">
<label class="text-white/80">Heal amount</label>
<input id="propHealAmount" type="number" min="0" class="p-1 rounded bg-[#0f1620] text-white text-sm w-24" />
</div>
</div>
<div class="border-t border-white/6 pt-2">
<h4 class="text-white/80 text-sm mb-2">Damage / Stats / Bonuses</h4>
<div class="flex items-center gap-3">
<label class="text-white/80">Damage</label>
<input id="propDamage" type="number" min="0" class="p-1 rounded bg-[#0f1620] text-white text-sm w-24" />
<label class="text-white/80 ml-3">Damage Bonus</label>
<input id="propDamageBonus" type="number" class="p-1 rounded bg-[#0f1620] text-white text-sm w-24" />
</div>
<div class="flex items-center gap-3 mt-2">
<label class="text-white/80">Attack</label>
<input id="statAttack" type="number" class="p-1 rounded bg-[#0f1620] text-white text-sm w-24" />
<label class="text-white/80 ml-3">HP</label>
<input id="statHp" type="number" class="p-1 rounded bg-[#0f1620] text-white text-sm w-24" />
<label class="text-white/80 ml-3">Defense</label>
<input id="statDefense" type="number" class="p-1 rounded bg-[#0f1620] text-white text-sm w-24" />
</div>
<div class="flex items-center gap-3 mt-2">
<label class="text-white/80">XP Reward</label>
<input id="statXpReward" type="number" class="p-1 rounded bg-[#0f1620] text-white text-sm w-24" />
</div>
</div>
<div class="border-t border-white/6 pt-2">
<h4 class="text-white/80 text-sm mb-2">Requirements</h4>
<div class="flex items-center gap-3">
<input type="checkbox" id="reqToolRequired" />
<label for="reqToolRequired" class="text-white/80">Require Tool</label>
<label class="text-white/80 ml-3">Tool Type</label>
<input id="reqToolType" placeholder="pickaxe/sword" class="p-1 rounded bg-[#0f1620] text-white text-sm" />
<label class="text-white/80 ml-3">Min Tier</label>
<input id="reqMinTier" type="number" min="0" class="p-1 rounded bg-[#0f1620] text-white text-sm w-24" />
</div>
</div>
<div class="border-t border-white/6 pt-2">
<h4 class="text-white/80 text-sm mb-2">Economy</h4>
<div class="flex items-center gap-3">
<label class="text-white/80">Sell Price</label>
<input id="propSellPrice" type="number" min="0" class="p-1 rounded bg-[#0f1620] text-white text-sm w-28" />
<label class="text-white/80 ml-3">Buy Price</label>
<input id="propBuyPrice" type="number" min="0" class="p-1 rounded bg-[#0f1620] text-white text-sm w-28" />
</div>
</div>
<div class="border-t border-white/6 pt-2">
<h4 class="text-white/80 text-sm mb-2">Chest / Drops</h4>
<div class="flex items-center gap-3">
<input type="checkbox" id="propChestEnabled" />
<label for="propChestEnabled" class="text-white/80">Chest enabled</label>
</div>
<div class="mt-2">
<label class="text-white/80">Rewards</label>
<div id="rewardsList" class="space-y-2 mt-2"></div>
<div class="flex gap-2 mt-2">
<select id="newRewardType" class="p-1 rounded bg-[#0f1620] text-white text-sm">
<option value="coins">Coins</option>
<option value="item">Item</option>
</select>
<input id="newRewardAmount" placeholder="amount" class="p-1 rounded bg-[#0f1620] text-white text-sm w-32" />
<input id="newRewardItemKey" placeholder="item.key (if item)" class="p-1 rounded bg-[#0f1620] text-white text-sm" />
<button type="button" id="addRewardBtn" class="px-2 py-1 rounded bg-indigo-600 text-white text-sm">Añadir</button>
</div>
</div>
</div>
<div>
<button type="button" id="loadRawPropsBtn" class="px-2 py-1 rounded bg-sky-600 text-white text-sm">Cargar props (admin)</button>
<span id="loadRawMsg" class="text-sm text-white/60 ml-3"></span>
</div>
</div>
</div>
<div class="col-span-2">
<label class="text-white/80">Metadata (UI)</label>
<div class="w-full mt-1 p-3 rounded bg-[#0f1720] text-white">
<div class="flex items-center gap-3">
<label class="text-white/80">Rarity</label>
<select id="metaRarity" class="p-1 rounded bg-[#0f1620] text-white text-sm">
<option value="common">common</option>
<option value="uncommon">uncommon</option>
<option value="rare">rare</option>
<option value="epic">epic</option>
</select>
<label class="text-white/80 ml-3">Weight</label>
<input id="metaWeight" type="number" min="0" class="p-1 rounded bg-[#0f1620] text-white text-sm w-24" />
</div>
<div class="mt-2">
<label class="text-white/80">Custom Metadata (advanced)</label>
<textarea id="metaCustomJson" class="w-full mt-1 p-2 rounded bg-[#0f1720] text-white font-mono" rows="3">{}</textarea>
</div>
</div>
</div>
</div>
<div class="mt-4 flex items-center justify-end gap-2">
<button type="button" id="cancelItemBtn" class="px-3 py-1 rounded bg-white/6 text-white">Cancelar</button>
<button type="submit" id="saveItemBtn" class="px-3 py-1 rounded bg-green-600 text-white">Guardar</button>
</div>
</form>
</div>
</div>
<script>
(function(){
const guildId = '<%= selectedGuildId ? selectedGuildId : '' %>';
let cachedItems = [];
let currentOpenEditId = 0;
function setModalError(msg) {
const el = document.getElementById('modalError');
if (!el) return;
el.textContent = msg || '';
if (msg) el.classList.remove('hidden'); else el.classList.add('hidden');
}
function clearModalError() { setModalError(''); }
const list = document.getElementById('itemsList');
const modal = document.getElementById('itemModal');
const form = document.getElementById('itemForm');
const createBtn = document.getElementById('createItemBtn');
const cancelBtn = document.getElementById('cancelItemBtn');
const modalTitle = document.getElementById('modalTitle');
function showModal(edit=false) {
modal.classList.remove('hidden');
modal.classList.add('flex');
}
function hideModal() {
modal.classList.add('hidden');
modal.classList.remove('flex');
form.reset();
// reset structured props/meta
document.getElementById('propCraftable').checked = false;
document.getElementById('propRecipeKey').value = '';
document.getElementById('propEquipable').checked = false;
document.getElementById('propSlot').value = '';
document.getElementById('propAttack').value = '';
document.getElementById('propDefense').value = '';
document.getElementById('propDurability').value = '';
document.getElementById('propMaxDurability').value = '';
document.getElementById('propCustomJson').value = '{}';
// new fields
document.getElementById('propToolType').value = '';
document.getElementById('propToolTier').value = '';
document.getElementById('propBreakable').checked = false;
document.getElementById('propDurabilityPerUse').value = '';
document.getElementById('propUsable').checked = false;
document.getElementById('propPurgeAllEffects').checked = false;
document.getElementById('propHealAmount').value = '';
document.getElementById('propDamage').value = '';
document.getElementById('propDamageBonus').value = '';
document.getElementById('statAttack').value = '';
document.getElementById('statHp').value = '';
document.getElementById('statDefense').value = '';
document.getElementById('statXpReward').value = '';
document.getElementById('reqToolRequired').checked = false;
document.getElementById('reqToolType').value = '';
document.getElementById('reqMinTier').value = '';
document.getElementById('propSellPrice').value = '';
document.getElementById('propBuyPrice').value = '';
document.getElementById('propChestEnabled').checked = false;
// rewards UI reset
const rewardsEl = document.getElementById('rewardsList');
if (rewardsEl) { rewardsEl.innerHTML = ''; rewardsEl.dataset.rewards = JSON.stringify([]); }
document.getElementById('newRewardType').value = 'coins';
document.getElementById('newRewardAmount').value = '';
document.getElementById('newRewardItemKey').value = '';
document.getElementById('metaRarity').value = 'common';
document.getElementById('metaWeight').value = '';
document.getElementById('metaCustomJson').value = '{}';
document.getElementById('itemId').value = '';
}
async function fetchItems() {
if (!guildId) return;
list.innerHTML = '<div class="text-white/60">Cargando items...</div>';
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.ok) throw new Error('bad');
cachedItems = j.items || [];
renderList(cachedItems);
} catch (err) {
list.innerHTML = '<div class="text-red-400">Error cargando items</div>';
}
}
// Rewards UI helpers
function renderRewardsList(rewards) {
const container = document.getElementById('rewardsList');
container.innerHTML = '';
if (!Array.isArray(rewards) || rewards.length === 0) return;
rewards.forEach((r, idx) => {
const row = document.createElement('div');
row.className = 'flex items-center gap-2';
if (r.type === 'coins' || typeof r.coins !== 'undefined') {
const amt = r.coins || r.amount || 0;
row.innerHTML = `<div class="text-white/80">Coins: <strong class="text-white">${amt}</strong></div>`;
} else if (r.type === 'item' || r.itemKey) {
const key = r.itemKey || (r.items && r.items[0] && r.items[0].key) || '';
const qty = r.qty || (r.items && r.items[0] && r.items[0].quantity) || r.quantity || 1;
row.innerHTML = `<div class="text-white/80">Item: <strong class="text-white">${escapeHtml(key)}</strong> x${qty}</div>`;
} else {
row.innerHTML = `<div class="text-white/80">${escapeHtml(JSON.stringify(r))}</div>`;
}
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 list = getCurrentRewards();
list.splice(idx,1);
renderRewardsList(list);
setCurrentRewards(list);
renderTagChips();
});
row.appendChild(del);
container.appendChild(row);
});
}
function getCurrentRewards() {
try {
const el = document.getElementById('rewardsList');
const nodes = Array.from(el.children || []);
// store rewards in dataset for simple persistence
const raw = el.dataset.rewards;
return raw ? JSON.parse(raw) : [];
} catch (e) { return []; }
}
function setCurrentRewards(arr) { document.getElementById('rewardsList').dataset.rewards = JSON.stringify(arr || []); }
document.getElementById('addRewardBtn').addEventListener('click', ()=>{
clearModalError();
const type = document.getElementById('newRewardType').value;
const amtRaw = document.getElementById('newRewardAmount').value;
const amt = Number(amtRaw);
const key = document.getElementById('newRewardItemKey').value.trim();
const list = getCurrentRewards();
if (type === 'coins') {
if (!amtRaw || isNaN(amt) || amt <= 0) { setModalError('Cantidad de coins debe ser mayor a 0'); return; }
list.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; }
list.push({ items: [{ key, quantity: amt || 1 }] });
}
setCurrentRewards(list);
renderRewardsList(list);
// clear input
document.getElementById('newRewardAmount').value = '';
document.getElementById('newRewardItemKey').value = '';
clearModalError();
});
// Hide item key input when reward type is coins
const newRewardTypeEl = document.getElementById('newRewardType');
const newRewardItemKeyEl = document.getElementById('newRewardItemKey');
function updateRewardKeyVisibility() {
if (!newRewardTypeEl || !newRewardItemKeyEl) return;
if (newRewardTypeEl.value === 'coins') {
newRewardItemKeyEl.disabled = true;
newRewardItemKeyEl.classList.add('opacity-50');
} else {
newRewardItemKeyEl.disabled = false;
newRewardItemKeyEl.classList.remove('opacity-50');
}
}
newRewardTypeEl.addEventListener('change', ()=>{ updateRewardKeyVisibility(); clearModalError(); });
updateRewardKeyVisibility();
// Tag chips UI
const tagsChipsContainer = document.createElement('div');
tagsChipsContainer.id = 'tagsChips';
tagsChipsContainer.className = 'mt-2 flex flex-wrap gap-2';
const tagsField = document.getElementById('fieldTags');
tagsField.parentNode.appendChild(tagsChipsContainer);
function renderTagChips() {
const base = tagsField.value.split(',').map(s=>s.trim()).filter(Boolean);
const auto = [];
// build preview from current UI selections
if (document.getElementById('propCraftable').checked) auto.push('craftable');
if (document.getElementById('propToolType').value.trim()) auto.push('tool');
if (document.getElementById('propDamage').value.trim() || document.getElementById('propAttack').value.trim()) auto.push('weapon');
if (document.getElementById('propUsable').checked) auto.push('consumable');
if (document.getElementById('propEquipable').checked) auto.push('equipable');
if (document.getElementById('propChestEnabled').checked) auto.push('chest');
if (document.getElementById('propBreakable').checked) auto.push('breakable');
if (document.getElementById('propSellPrice').value.trim()) auto.push('sellable');
if (document.getElementById('propBuyPrice').value.trim()) auto.push('buyable');
const merged = Array.from(new Set([...base, ...auto]));
tagsChipsContainer.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;
tagsChipsContainer.appendChild(chip);
});
}
// update chips when some inputs change
['propCraftable','propToolType','propDamage','propAttack','propUsable','propEquipable','propChestEnabled','propBreakable','propSellPrice','propBuyPrice','fieldTags'].forEach(id=>{
const el = document.getElementById(id);
if (!el) return;
el.addEventListener('input', renderTagChips);
el.addEventListener('change', renderTagChips);
});
function renderList(items) {
if (!Array.isArray(items) || items.length === 0) {
list.innerHTML = '<div class="text-white/60">No hay items definidos.</div>';
return;
}
list.innerHTML = '';
for (const it of items) {
const el = document.createElement('div');
el.className = 'p-3 bg-[#071a2a] rounded flex items-start justify-between';
el.innerHTML = `<div>
<div class="text-white font-medium">${escapeHtml(it.name)} <span class="text-white/50">(${escapeHtml(it.key)})</span></div>
<div class="text-white/60 text-sm mt-1">${escapeHtml(it.description || '')}</div>
</div>
<div class="flex items-center gap-2">
<button data-id="${it.id}" class="editBtn px-2 py-1 bg-indigo-600 rounded text-white text-sm">Editar</button>
<button data-id="${it.id}" class="delBtn px-2 py-1 bg-red-600 rounded text-white text-sm">Eliminar</button>
</div>`;
list.appendChild(el);
}
// attach handlers
Array.from(list.querySelectorAll('.editBtn')).forEach(b=>b.addEventListener('click', onEdit));
Array.from(list.querySelectorAll('.delBtn')).forEach(b=>b.addEventListener('click', onDelete));
}
// Page-level alerts (non-blocking)
function showPageAlert(type, msg, ttl) {
try {
const c = document.getElementById('pageAlert');
if (!c) return;
const color = (type === 'success') ? 'bg-green-600' : (type === 'danger' ? 'bg-red-700' : (type === 'warning' ? 'bg-amber-700' : 'bg-sky-600'));
c.innerHTML = `<div class="p-3 rounded ${color} text-white flex items-center justify-between"><div>${escapeHtml(msg)}</div><button id=\"pageAlertClose\" class=\"ml-4 font-bold\">✕</button></div>`;
c.classList.remove('hidden');
const close = document.getElementById('pageAlertClose');
if (close) close.addEventListener('click', ()=>{ c.classList.add('hidden'); c.innerHTML = ''; });
if (typeof ttl === 'number' && ttl > 0) setTimeout(()=>{ c.classList.add('hidden'); c.innerHTML = ''; }, ttl);
} catch (e) { console.error('showPageAlert error', e); }
}
function clearPageAlert() { const c = document.getElementById('pageAlert'); if (c) { c.classList.add('hidden'); c.innerHTML=''; } }
function escapeHtml(s){ if (!s) return ''; return String(s).replace(/[&<>"']/g, (m)=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'})[m] || m); }
function onEdit(e) {
const id = e.currentTarget.getAttribute('data-id');
openEdit(id);
}
async function openEdit(id) {
clearModalError();
try {
// Prefer using cached items to avoid an extra network call and potential race conditions
let it = (cachedItems || []).find(x=>String(x.id) === String(id));
if (!it) {
// fallback to fetch single list if not in cache
try {
const res = await fetch(`/api/dashboard/${encodeURIComponent(guildId)}/items`);
if (res && res.ok) {
const j = await res.json();
it = (j.items || []).find(x=>String(x.id) === String(id));
}
} catch (e) {
// ignore and continue
}
}
if (!it) { setModalError('Item no encontrado'); return; }
document.getElementById('itemId').value = it.id;
document.getElementById('fieldKey').value = it.key || '';
document.getElementById('fieldName').value = it.name || '';
document.getElementById('fieldCategory').value = it.category || '';
document.getElementById('fieldIcon').value = it.icon || '';
document.getElementById('fieldDescription').value = it.description || '';
document.getElementById('fieldTags').value = (Array.isArray(it.tags) ? it.tags.join(',') : '');
document.getElementById('fieldMaxPer').value = it.maxPerInventory || '';
// For security we do NOT expose stored props/metadata via the API. UI fields will be empty/default and admin can attempt raw load.
document.getElementById('propCraftable').checked = false;
document.getElementById('propRecipeKey').value = '';
document.getElementById('propEquipable').checked = false;
document.getElementById('propSlot').value = '';
document.getElementById('propAttack').value = '';
document.getElementById('propDefense').value = '';
document.getElementById('propDurability').value = '';
document.getElementById('propMaxDurability').value = '';
document.getElementById('propCustomJson').value = '{}';
document.getElementById('metaRarity').value = 'common';
document.getElementById('metaWeight').value = '';
document.getElementById('metaCustomJson').value = '{}';
// new fields defaults on edit
document.getElementById('propToolType').value = '';
document.getElementById('propToolTier').value = '';
document.getElementById('propBreakable').checked = false;
document.getElementById('propDurabilityPerUse').value = '';
document.getElementById('propUsable').checked = false;
document.getElementById('propPurgeAllEffects').checked = false;
document.getElementById('propHealAmount').value = '';
document.getElementById('propDamage').value = '';
document.getElementById('propDamageBonus').value = '';
document.getElementById('statAttack').value = '';
document.getElementById('statHp').value = '';
document.getElementById('statDefense').value = '';
document.getElementById('statXpReward').value = '';
document.getElementById('reqToolRequired').checked = false;
document.getElementById('reqToolType').value = '';
document.getElementById('reqMinTier').value = '';
document.getElementById('propSellPrice').value = '';
document.getElementById('propBuyPrice').value = '';
document.getElementById('propChestEnabled').checked = false;
document.getElementById('propDropsJson').value = '{}';
// populate rewards if we have a cached representation in the item (not exposed by default)
document.getElementById('rewardsList').dataset.rewards = JSON.stringify([]);
renderRewardsList([]);
modalTitle.textContent = 'Editar item';
showModal(true);
} catch (err) {
setModalError('Error al abrir item');
}
}
// Try to load raw props for the currently opened item (admin-only; server may reject)
async function tryLoadRawProps(itemId) {
const msgEl = document.getElementById('loadRawMsg');
msgEl.textContent = '';
try {
const res = await fetch(`/api/dashboard/${encodeURIComponent(guildId)}/items/${encodeURIComponent(itemId)}?raw=1`, { headers: { 'Accept':'application/json' } });
if (!res.ok) {
// server might reject raw access; silent friendly message
msgEl.textContent = 'raw no permitido';
return null;
}
const j = await res.json();
if (!j || !j.ok) { msgEl.textContent = 'no data'; return null; }
msgEl.textContent = 'raw cargado';
return j.item || null;
} catch (e) {
msgEl.textContent = 'error';
return null;
}
}
async function onDelete(e) {
const id = e.currentTarget.getAttribute('data-id');
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();
} catch (err) {
showPageAlert('danger','Error al eliminar');
}
}
createBtn.addEventListener('click', ()=>{ clearModalError(); currentOpenEditId++; hideModal(); modalTitle.textContent = 'Crear item'; showModal(false); });
cancelBtn.addEventListener('click', ()=>{ hideModal(); clearModalError(); });
form.addEventListener('submit', async (ev)=>{
async function openEdit(id) {
clearModalError();
const myOpenId = ++currentOpenEditId;
try {
// Prefer using cached items to avoid an extra network call and potential race conditions
let it = (cachedItems || []).find(x=>String(x.id) === String(id));
if (!it) {
// fallback to fetch single list if not in cache
try {
const res = await fetch(`/api/dashboard/${encodeURIComponent(guildId)}/items`);
if (res && res.ok) {
const j = await res.json();
it = (j.items || []).find(x=>String(x.id) === String(id));
}
} catch (e) {
// ignore and continue
}
}
// if another open request happened meanwhile, abort applying
if (myOpenId !== currentOpenEditId) return;
if (!it) { setModalError('Item no encontrado'); return; }
document.getElementById('itemId').value = it.id;
document.getElementById('fieldKey').value = it.key || '';
document.getElementById('fieldName').value = it.name || '';
document.getElementById('fieldCategory').value = it.category || '';
document.getElementById('fieldIcon').value = it.icon || '';
document.getElementById('fieldDescription').value = it.description || '';
document.getElementById('fieldTags').value = (Array.isArray(it.tags) ? it.tags.join(',') : '');
document.getElementById('fieldMaxPer').value = it.maxPerInventory || '';
// For security we do NOT expose stored props/metadata via the API. UI fields will be empty/default and admin can attempt raw load.
document.getElementById('propCraftable').checked = false;
document.getElementById('propRecipeKey').value = '';
document.getElementById('propEquipable').checked = false;
document.getElementById('propSlot').value = '';
document.getElementById('propAttack').value = '';
document.getElementById('propDefense').value = '';
document.getElementById('propDurability').value = '';
document.getElementById('propMaxDurability').value = '';
document.getElementById('propCustomJson').value = '{}';
document.getElementById('metaRarity').value = 'common';
document.getElementById('metaWeight').value = '';
document.getElementById('metaCustomJson').value = '{}';
// new fields defaults on edit
document.getElementById('propToolType').value = '';
document.getElementById('propToolTier').value = '';
document.getElementById('propBreakable').checked = false;
document.getElementById('propDurabilityPerUse').value = '';
document.getElementById('propUsable').checked = false;
document.getElementById('propPurgeAllEffects').checked = false;
document.getElementById('propHealAmount').value = '';
document.getElementById('propDamage').value = '';
document.getElementById('propDamageBonus').value = '';
document.getElementById('statAttack').value = '';
document.getElementById('statHp').value = '';
document.getElementById('statDefense').value = '';
document.getElementById('statXpReward').value = '';
document.getElementById('reqToolRequired').checked = false;
document.getElementById('reqToolType').value = '';
document.getElementById('reqMinTier').value = '';
document.getElementById('propSellPrice').value = '';
document.getElementById('propBuyPrice').value = '';
document.getElementById('propChestEnabled').checked = false;
document.getElementById('propDropsJson').value = '{}';
// populate rewards if we have a cached representation in the item (not exposed by default)
const rewardsEl2 = document.getElementById('rewardsList');
if (rewardsEl2) { rewardsEl2.dataset.rewards = JSON.stringify([]); }
renderRewardsList([]);
modalTitle.textContent = 'Editar item';
showModal(true);
} catch (err) {
setModalError('Error al abrir item');
}
}
}
}
} catch (e) { setModalError('JSON inválido en Drops/Chest: ' + (e && e.message ? e.message : String(e))); return; }
// merge custom JSON
try {
const custom = JSON.parse(document.getElementById('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; }
// Build metadata
let parsedMeta = { rarity: document.getElementById('metaRarity').value };
const w = document.getElementById('metaWeight').value.trim(); if (w) parsedMeta.weight = Number(w);
try {
const customM = JSON.parse(document.getElementById('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: document.getElementById('fieldKey').value.trim(),
name: document.getElementById('fieldName').value.trim(),
category: document.getElementById('fieldCategory').value.trim(),
icon: document.getElementById('fieldIcon').value.trim(),
description: document.getElementById('fieldDescription').value.trim(),
tags: document.getElementById('fieldTags').value.split(',').map(s=>s.trim()).filter(Boolean),
maxPerInventory: Number(document.getElementById('fieldMaxPer').value) || null,
props: Object.keys(parsedProps).length ? parsedProps : null,
metadata: Object.keys(parsedMeta).length ? parsedMeta : null,
};
// Auto-generate tags from props UI
try {
const autoTags = new Set(payload.tags);
if (parsedProps.craftable) autoTags.add('craftable');
if (parsedProps.tool) autoTags.add('tool');
if (parsedProps.damage || parsedProps.attack || parsedProps.stats) autoTags.add('weapon');
if (parsedProps.usable) autoTags.add('consumable');
if (parsedProps.equipable) autoTags.add('equipable');
if (parsedProps.chest) autoTags.add('chest');
if (parsedProps.breakable) autoTags.add('breakable');
if (parsedProps.sellPrice) autoTags.add('sellable');
if (parsedProps.buyPrice) autoTags.add('buyable');
// merge back to payload.tags as array
payload.tags = Array.from(autoTags).filter(Boolean);
} catch (e) {
// ignore tagging failures
}
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');
}
hideModal();
clearModalError();
await fetchItems();
} catch (err) {
setModalError('Error al guardar item. ¿JSON válido en Props/Metadata?');
}
});
// load raw props button handler
document.getElementById('loadRawPropsBtn').addEventListener('click', async ()=>{
const id = document.getElementById('itemId').value;
if (!id) { setModalError('Abra un item primero para cargar props raw'); return; }
clearPageAlert();
const raw = await tryLoadRawProps(id);
if (!raw) return;
try {
if (raw.props && typeof raw.props === 'object') {
// map known props to UI
document.getElementById('propCraftable').checked = !!raw.props.craftable;
document.getElementById('propRecipeKey').value = raw.props.recipe && raw.props.recipe.key ? raw.props.recipe.key : '';
document.getElementById('propEquipable').checked = !!raw.props.equipable;
document.getElementById('propSlot').value = raw.props.slot || '';
document.getElementById('propAttack').value = raw.props.attack || '';
document.getElementById('propDefense').value = raw.props.defense || '';
document.getElementById('propDurability').value = raw.props.durability || '';
document.getElementById('propMaxDurability').value = raw.props.maxDurability || '';
// tool
document.getElementById('propToolType').value = (raw.props.tool && raw.props.tool.type) ? raw.props.tool.type : '';
document.getElementById('propToolTier').value = (raw.props.tool && typeof raw.props.tool.tier !== 'undefined') ? String(raw.props.tool.tier) : '';
// breakable
document.getElementById('propBreakable').checked = !!(raw.props.breakable && raw.props.breakable.enabled);
document.getElementById('propDurabilityPerUse').value = (raw.props.breakable && raw.props.breakable.durabilityPerUse) ? String(raw.props.breakable.durabilityPerUse) : '';
// usable
document.getElementById('propUsable').checked = !!raw.props.usable;
document.getElementById('propPurgeAllEffects').checked = !!raw.props.purgeAllEffects;
document.getElementById('propHealAmount').value = raw.props.heal || '';
// damage/bonuses
document.getElementById('propDamage').value = raw.props.damage || '';
document.getElementById('propDamageBonus').value = raw.props.damageBonus || '';
// stats
document.getElementById('statAttack').value = (raw.props.stats && raw.props.stats.attack) ? String(raw.props.stats.attack) : '';
document.getElementById('statHp').value = (raw.props.stats && raw.props.stats.hp) ? String(raw.props.stats.hp) : '';
document.getElementById('statDefense').value = (raw.props.stats && raw.props.stats.defense) ? String(raw.props.stats.defense) : '';
document.getElementById('statXpReward').value = (raw.props.stats && raw.props.stats.xpReward) ? String(raw.props.stats.xpReward) : '';
// requirements
document.getElementById('reqToolRequired').checked = !!(raw.props.requirements && raw.props.requirements.tool && raw.props.requirements.tool.required);
document.getElementById('reqToolType').value = (raw.props.requirements && raw.props.requirements.tool && raw.props.requirements.tool.toolType) ? raw.props.requirements.tool.toolType : '';
document.getElementById('reqMinTier').value = (raw.props.requirements && raw.props.requirements.tool && typeof raw.props.requirements.tool.minTier !== 'undefined') ? String(raw.props.requirements.tool.minTier) : '';
// economy
document.getElementById('propSellPrice').value = raw.props.sellPrice || '';
document.getElementById('propBuyPrice').value = raw.props.buyPrice || '';
// chest/drops
document.getElementById('propChestEnabled').checked = !!(raw.props.chest && raw.props.chest.enabled);
// map rewards into UI
const rewards = raw.props.chest && raw.props.chest.rewards ? raw.props.chest.rewards : [];
document.getElementById('rewardsList').dataset.rewards = JSON.stringify(rewards || []);
renderRewardsList(rewards || []);
// leave extra fields in custom JSON
const copy = Object.assign({}, raw.props);
delete copy.craftable; delete copy.recipe; delete copy.equipable; delete copy.slot; delete copy.attack; delete copy.defense; delete copy.durability; delete copy.maxDurability;
document.getElementById('propCustomJson').value = JSON.stringify(copy, null, 2);
}
if (raw.metadata && typeof raw.metadata === 'object') {
document.getElementById('metaRarity').value = raw.metadata.rarity || 'common';
document.getElementById('metaWeight').value = raw.metadata.weight || '';
const copyM = Object.assign({}, raw.metadata);
delete copyM.rarity; delete copyM.weight;
document.getElementById('metaCustomJson').value = JSON.stringify(copyM, null, 2);
}
} catch (e) {
setModalError('Error al mapear raw props: ' + (e && e.message ? e.message : String(e)));
}
});
// Initial load
fetchItems();
})();
</script>
<script src="/assets/js/dashboard_items.js" defer></script>
</div>

View File

@@ -1,4 +1,4 @@
<div class="w-full max-w-3xl mt-6 mx-auto">
<div class="w-full max-w-3xl mt-6 mx-auto bg-gray-800/50">
<div class="backdrop-blur-md bg-white/6 rounded-xl p-6 glass-card">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-semibold">Ajustes del servidor</h2>

20
tmp_acorn.js Normal file
View File

@@ -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(/<script[^>]*>([\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<end;i++){
const n = i+1; console.error((n===L? '>' : ' ') + n.toString().padStart(4,' ') + '| ' + lines[i]);
}
}
}

43
tmp_check.js Normal file
View File

@@ -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(/<script[^>]*>([\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.length;i++){
const ch = src[i];
const next = src[i+1] || '';
if(inComment){ if(ch==='*' && next==='/' ){ inComment=false; i++; continue; } else continue; }
if(inLineComment){ if(ch==='\n'){ inLineComment=false; continue; } else continue; }
if(escape){ escape=false; continue; }
if(ch==='\\') { escape=true; continue; }
if(!inSingle && !inDouble && !inTemplate){
if(ch==='/' && next==='*'){ inComment=true; i++; continue; }
if(ch==='/' && next==='/'){ inLineComment=true; i++; continue; }
}
if(!inDouble && !inTemplate && ch==="'") { inSingle = !inSingle; if(inSingle) lastSingle = i; continue; }
if(!inSingle && !inTemplate && ch==='"') { inDouble = !inDouble; if(inDouble) lastDouble = i; continue; }
if(!inSingle && !inDouble && ch==='`') { inTemplate = !inTemplate; if(inTemplate) lastTemplate = i; continue; }
if(inSingle || inDouble || inTemplate) continue;
if(opens[ch]){ stack.push({ch, i}); continue; }
if(closes[ch]){
if(stack.length===0){ console.error('UNMATCHED_CLOSE', ch, 'at', i); process.exit(3); }
const top = stack.pop();
if(top.ch !== closes[ch]){ console.error('MISMATCH', top, 'close', ch, 'at', i); process.exit(4); }
}
}
if(inSingle || inDouble || inTemplate) {
console.error('UNTERMINATED_STRING');
if(inSingle) console.error('lastSingle@', lastSingle, 'context=>', 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);

11
tmp_find_parse_error.js Normal file
View File

@@ -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(/<script[^>]*>([\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<high){ const mid=Math.floor((low+high)/2); try{ acorn.parse(src.slice(0,mid),{ecmaVersion:2020}); low=mid+1; }catch(err){ bad=mid; high=mid; }}
console.log('first bad index approx', bad);
const start=Math.max(0,bad-120), end=Math.min(src.length,bad+120);
const snippet=src.slice(start,end);
console.log('snippet around bad:\n', snippet.replace(/\n/g,'\n'));
// also show line/col of bad via counting
const pre=src.slice(0,bad); const lines=pre.split('\n'); console.log('line',lines.length,'col',lines[lines.length-1].length+1);

10
tmp_line_parse.js Normal file
View File

@@ -0,0 +1,10 @@
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(/<script[^>]*>([\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');

11
tmp_print_script.js Normal file
View File

@@ -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(/<script[^>]*>([\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<lines.length;i++){
console.log((i+1).toString().padStart(4,' ')+': '+lines[i]);
}
console.log('TOTAL LINES:', lines.length);

17
tmp_token_balance.js Normal file
View File

@@ -0,0 +1,17 @@
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(/<script[^>]*>([\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');

20
tmp_token_trace.js Normal file
View File

@@ -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(/<script[^>]*>([\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);

8
tmp_tokens_inspect.js Normal file
View File

@@ -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(/<script[^>]*>([\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');