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:
84
.validate_dashboard_items_syntax.js
Normal file
84
.validate_dashboard_items_syntax.js
Normal 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
1
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
212
src/server/public/assets/js/dashboard_items.js
Normal file
212
src/server/public/assets/js/dashboard_items.js
Normal 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 = {'&':'&','<':'<','>':'>','"':'"',"'":"'"};
|
||||
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();
|
||||
})();
|
||||
@@ -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)=>({'&':'&','<':'<','>':'>','"':'"',"'":'''})[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>
|
||||
|
||||
@@ -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
20
tmp_acorn.js
Normal 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
43
tmp_check.js
Normal 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
11
tmp_find_parse_error.js
Normal 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
10
tmp_line_parse.js
Normal 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
11
tmp_print_script.js
Normal 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
17
tmp_token_balance.js
Normal 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
20
tmp_token_trace.js
Normal 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
8
tmp_tokens_inspect.js
Normal 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');
|
||||
Reference in New Issue
Block a user