feat: Actualizar la tipografía y mejorar la selección de roles en el panel de configuración

This commit is contained in:
Shni
2025-10-15 10:10:41 -05:00
parent 2a67d249fb
commit e2a7013ee0
2 changed files with 145 additions and 25 deletions

View File

@@ -76,7 +76,7 @@ body::before {
/* Tipografía Moderna */
h1 {
font-family: 'Press Start 2P', monospace;
font-family: 'BoldPixels', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
font-size: clamp(2rem, 5vw, 4rem);
font-weight: 400;
line-height: 1.3;

View File

@@ -13,33 +13,23 @@
<label class="block text-sm text-slate-200 mb-1">AI Role Prompt (opcional)</label>
<textarea name="aiRolePrompt" id="aiRoleInput" rows="4" class="w-full rounded p-2 bg-transparent border border-white/6" placeholder="E.g. Actúa como un moderador amigable..."><%= (guildConfig && guildConfig.aiRolePrompt) || '' %></textarea>
</div>
<div>
<label class="block text-sm text-slate-200 mb-1">Roles de staff</label>
<% const selectedStaff = (guildConfig && Array.isArray(guildConfig.staff) ? guildConfig.staff.map(String) : (guildConfig && guildConfig.staff ? String(guildConfig.staff).split(',') : [])) || []; %>
<% if (typeof guildRoles !== 'undefined' && guildRoles && guildRoles.length) { %>
<div class="mb-2">
<input id="staffFilter" type="search" placeholder="Filtrar roles..." class="w-full rounded p-2 bg-transparent border border-white/6" />
<div id="staffTagsContainer" class="w-full rounded p-2 bg-transparent border border-white/6">
<div id="staffChips" class="flex flex-wrap gap-2"></div>
<input id="staffTagInput" type="text" placeholder="Buscar roles..." autocomplete="off" class="w-full bg-transparent outline-none text-sm p-1" />
</div>
<select id="staffSelect" name="staffSelect" multiple class="w-full rounded p-2 bg-transparent border border-white/6 h-36">
<% const selectedStaff = (guildConfig && Array.isArray(guildConfig.staff) ? guildConfig.staff.map(String) : (guildConfig && guildConfig.staff ? String(guildConfig.staff).split(',') : [])) || []; %>
<% guildRoles.forEach(r => { %>
<option value="<%= r.id %>" <%= selectedStaff.includes(String(r.id)) ? 'selected' : '' %>><%= r.name %> — <%= r.id %></option>
<% }) %>
</select>
<% } else { %>
<% const fallbackStaff = (guildConfig && Array.isArray(guildConfig.staff) ? guildConfig.staff.map(String) : (guildConfig && guildConfig.staff ? String(guildConfig.staff).split(',') : [])) || []; %>
<% if (fallbackStaff.length) { %>
<div class="mb-2">
<input id="staffFilter" type="search" placeholder="Filtrar roles..." class="w-full rounded p-2 bg-transparent border border-white/6" />
<ul id="staffSuggestions" class="mt-1 bg-white/4 rounded max-h-40 overflow-auto hidden z-50"></ul>
<input type="hidden" id="staffHidden" name="staff" value="<%= selectedStaff.join(',') %>" />
<div class="text-xs text-slate-300 mt-1">Selecciona roles del servidor. Escribe para filtrar, pulsa Enter para elegir.</div>
</div>
<select id="staffSelect" name="staffSelect" multiple class="w-full rounded p-2 bg-transparent border border-white/6 h-36">
<% fallbackStaff.forEach(id => { %>
<option value="<%= id %>" <%= (Array.isArray(guildConfig.staff) ? guildConfig.staff.map(String).includes(String(id)) : String(guildConfig.staff || '').split(',').map(s=>s.trim()).includes(String(id))) ? 'selected' : '' %>>ID: <%= id %></option>
<% }) %>
</select>
<% } else { %>
<input type="text" name="staff" id="staffInput" value="<%= (guildConfig && (Array.isArray(guildConfig.staff) ? guildConfig.staff.join(',') : guildConfig.staff)) || '' %>" class="w-full rounded p-2 bg-transparent border border-white/6" placeholder="123... , 456..." />
<div class="text-xs text-slate-300 mt-1">No se pudo obtener roles desde la API ni hay roles guardados. Introduce IDs manualmente separadas por coma.</div>
<% } %>
<div class="text-xs text-slate-300 mt-1">No se pudo obtener roles desde la API. Introduce IDs manualmente separadas por coma.</div>
<% } %>
</div>
<div class="flex items-center gap-3">
@@ -98,4 +88,134 @@
}
});
})();
(function(){
const form = document.getElementById('guildSettingsForm');
const status = document.getElementById('saveStatus');
// Multi-select tag/autocomplete for roles
const staffTagInput = document.getElementById('staffTagInput');
const staffSuggestions = document.getElementById('staffSuggestions');
const staffChips = document.getElementById('staffChips');
const staffHidden = document.getElementById('staffHidden');
// Prepare role options (id, name, color) from server-side `guildRoles`
<% const ROLE_JSON = JSON.stringify((guildRoles||[]).map(r=>{ var c; if (typeof r.color !== 'undefined' && r.color !== null) { if (typeof r.color === 'number') { c = '#'+('000000'+r.color.toString(16)).slice(-6); } else { c = (''+r.color).replace(/^#?/, '#'); } } else { c = (r.colorHex||r.hex||'#8b95a0'); } return { id: String(r.id), name: r.name, color: c }; })); %>
const ROLE_OPTIONS = <%- ROLE_JSON %>;
// Initial selected staff IDs (strings)
const INITIAL_STAFF = <%- JSON.stringify(selectedStaff || []) %>;
let selectedIds = Array.isArray(INITIAL_STAFF) ? INITIAL_STAFF.slice() : [];
function renderChips(){
staffChips.innerHTML = '';
for(const id of selectedIds){
const role = ROLE_OPTIONS.find(r=>r.id===String(id));
const label = role ? role.name : String(id);
const chip = document.createElement('div');
chip.className = 'px-2 py-1 bg-white/6 rounded-full text-sm flex items-center gap-2';
const swatch = role && role.color ? `<span class="w-3 h-3 rounded-full inline-block" style="background:${role.color}"></span>` : '';
chip.innerHTML = `${swatch}<span class="truncate">${label}</span><button type="button" class="ml-2 text-xs text-slate-300 remove-chip" data-id="${id}">✕</button>`;
staffChips.appendChild(chip);
}
// update hidden input
if(staffHidden) staffHidden.value = selectedIds.join(',');
}
function showSuggestions(query){
if(!staffSuggestions) return;
const q = (query||'').trim().toLowerCase();
const filtered = ROLE_OPTIONS.filter(r=> !selectedIds.includes(String(r.id)) && (!q || r.name.toLowerCase().includes(q) || String(r.id).includes(q)) ).slice(0,50);
if(!filtered.length){ staffSuggestions.classList.add('hidden'); staffSuggestions.innerHTML=''; return; }
staffSuggestions.classList.remove('hidden');
staffSuggestions.innerHTML = filtered.map(r=>`<li class="p-2 hover:bg-white/10 cursor-pointer flex items-center gap-2" data-id="${r.id}"><span class="w-3 h-3 rounded-full inline-block" style="background:${r.color}"></span><span class="truncate">${r.name}</span></li>`).join('');
}
function addId(id){
id = String(id);
if(!id) return;
if(selectedIds.includes(id)) return;
selectedIds.push(id);
renderChips();
}
function removeId(id){
id = String(id);
selectedIds = selectedIds.filter(i=>String(i)!==id);
renderChips();
}
if(staffTagInput){
// initial render
renderChips();
staffTagInput.addEventListener('input', (e)=>{
showSuggestions(e.target.value);
});
staffTagInput.addEventListener('keydown', (e)=>{
if(e.key === 'Enter'){
e.preventDefault();
// pick first suggestion if any, otherwise ignore
const first = staffSuggestions.querySelector('li');
if(first){ addId(first.dataset.id); staffTagInput.value=''; staffSuggestions.classList.add('hidden'); }
} else if (e.key === 'Backspace' && !staffTagInput.value){
// remove last
selectedIds.pop(); renderChips();
}
});
document.addEventListener('click', (ev)=>{
if(!staffSuggestions) return;
if(ev.target.closest && ev.target.closest('#staffSuggestions')) return;
if(ev.target === staffTagInput) return;
staffSuggestions.classList.add('hidden');
});
staffSuggestions.addEventListener('click', (ev)=>{
const li = ev.target.closest('li');
if(!li) return;
addId(li.dataset.id);
staffTagInput.value='';
staffSuggestions.classList.add('hidden');
staffTagInput.focus();
});
staffChips.addEventListener('click', (ev)=>{
const btn = ev.target.closest('.remove-chip');
if(!btn) return;
removeId(btn.dataset.id);
});
}
form.addEventListener('submit', async (e)=>{
e.preventDefault();
status.textContent = 'Guardando...';
const prefix = document.getElementById('prefixInput').value.trim();
const aiRolePrompt = document.getElementById('aiRoleInput').value.trim();
let staffArr = [];
if(typeof selectedIds !== 'undefined' && selectedIds && selectedIds.length){
staffArr = selectedIds.slice();
} else if (document.getElementById('staffInput')){
const staffRaw = document.getElementById('staffInput').value.trim();
staffArr = staffRaw ? staffRaw.split(',').map(s=>s.trim()).filter(Boolean) : [];
}
const payload = { prefix: prefix, aiRolePrompt: aiRolePrompt.length ? aiRolePrompt : null, staff: staffArr };
try {
const res = await fetch(`/api/dashboard/${encodeURIComponent('<%= selectedGuild %>')}/settings`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
const json = await res.json();
if (res.ok && json.ok) {
status.textContent = 'Guardado';
setTimeout(()=> status.textContent = '', 2500);
} else {
status.textContent = json.error || 'Error';
}
} catch (err) {
status.textContent = 'Error de red';
}
});
})();
</script>