Files
amayo/src/server/views/pages/dashboard.ejs

367 lines
20 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- dashboard_nav is rendered by the layout via renderTemplate to avoid unresolved Promise output -->
<div class="max-w-3xl mx-auto p-6">
<div class="relative flex justify-center">
<% if (!selectedGuild) { %>
<!-- Centered logo + selection card to match mock -->
<div class="flex flex-col items-center justify-start">
<!-- logo area -->
<div class="mt-10 mb-6">
<!-- reuse small logo SVG -->
<div class="w-12 h-12 mx-auto">
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" class="w-full h-full">
<defs>
<linearGradient id="g2" x1="0" x2="1">
<stop offset="0%" stop-color="#06b6d4" />
<stop offset="100%" stop-color="#3b82f6" />
</linearGradient>
</defs>
<circle cx="50" cy="50" r="42" fill="url(#g2)" opacity="0.95" />
<g transform="translate(50,50)">
<path d="M-18,-6 L0,-28 L18,-6 L0,18 Z" fill="white" opacity="0.95"/>
</g>
</svg>
</div>
</div>
<div class="w-full max-w-lg">
<div class="mx-auto backdrop-blur-md bg-white/6 border border-white/8 rounded-xl p-6 shadow-lg glass-card">
<!-- user header inside card -->
<div class="flex items-center justify-center mb-4">
<div class="text-slate-200 text-sm">Logged in as</div>
<div class="ml-3 relative">
<button id="cardUserBtn" class="flex items-center gap-2 px-3 py-1 rounded-md hover:bg-white/5 focus:outline-none" aria-expanded="false">
<% if (user && user.id && user.avatar) { %>
<img src="https://cdn.discordapp.com/avatars/<%= user.id %>/<%= user.avatar %>.png" class="w-8 h-8 rounded-full" alt="avatar">
<% } else { %>
<img src="/assets/images/snap1.svg" class="w-8 h-8 rounded-full" alt="avatar">
<% } %>
<span class="text-white font-medium"><%= user?.username || 'User' %></span>
<svg class="w-3 h-3 text-white/80" viewBox="0 0 20 20" fill="none"><path d="M6 8l4 4 4-4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<div id="cardUserMenu" class="absolute right-0 mt-2 w-36 bg-white/6 backdrop-blur rounded-md p-2 hidden">
<a href="/auth/logout" class="block px-3 py-2 text-sm text-rose-300 hover:bg-white/5 rounded">Log out</a>
</div>
</div>
</div>
<div id="cardList" class="divide-y divide-white/6 max-h-72 overflow-auto rounded">
<% if (guilds && guilds.length) { %>
<% guilds.sort((a,b)=> a.name.localeCompare(b.name)).forEach(g => { %>
<div class="p-3 cursor-pointer hover:bg-white/5 flex items-center justify-between text-white" data-id="<%= g.id %>">
<div class="flex items-center gap-3">
<% if (g.icon) { %>
<img src="https://cdn.discordapp.com/icons/<%= g.id %>/<%= g.icon %>.png" class="w-8 h-8 rounded-full" alt="icon">
<% } else { %>
<div class="w-8 h-8 rounded-full bg-white/8 flex items-center justify-center text-xs text-white">S</div>
<% } %>
<div class="text-left">
<div class="font-medium"><%= g.name %></div>
</div>
</div>
<div class="text-slate-300">&rsaquo;</div>
</div>
<% }) %>
<% } else { %>
<div class="p-4 text-slate-300">No servers available</div>
<% } %>
</div>
</div>
<div class="text-center text-xs text-slate-400 mt-6">© 20212025 Xge • Terms • Privacy • Legal Notice</div>
</div>
</div>
<script>
(function(){
const card = document.querySelector('[data-id]')?.closest('.mx-auto');
// user menu toggle inside card
const userBtn = document.getElementById('cardUserBtn');
const userMenu = document.getElementById('cardUserMenu');
if (userBtn && userMenu) {
userBtn.addEventListener('click',(e)=>{ e.stopPropagation(); const open = userMenu.classList.contains('hidden'); if (open) { userMenu.classList.remove('hidden'); } else { userMenu.classList.add('hidden'); } });
document.addEventListener('click', ()=> userMenu.classList.add('hidden'));
}
// clicking a guild row navigates
document.getElementById('cardList')?.addEventListener('click', (e)=>{
let el = e.target;
while (el && !el.dataset?.id) el = el.parentElement;
if (el && el.dataset && el.dataset.id) {
const id = el.dataset.id;
window.location.href = `/dashboard/${id}/overview`;
}
});
})();
</script>
<% } %>
<% if (typeof selectedGuild !== 'undefined' && selectedGuild) { %>
<!-- Floating sidebar for guild actions -->
<aside class="absolute right-6 top-12 hidden lg:block transform translate-y-1">
<nav class="bg-white/6 backdrop-blur rounded-xl p-4 glass-card w-48">
<ul class="flex flex-col gap-3">
<li><a href="/dashboard/<%= selectedGuild %>/overview" class="block p-2 rounded-md hover:bg-white/5">Overview</a></li>
<li><a href="/dashboard/<%= selectedGuild %>/members" class="block p-2 rounded-md hover:bg-white/5">Miembros</a></li>
<li><a href="/dashboard/<%= selectedGuild %>/settings" class="block p-2 rounded-md hover:bg-white/5">Ajustes</a></li>
<li><a href="/dashboard/<%= selectedGuild %>/areas" class="block p-2 rounded-md hover:bg-white/5">Game Areas</a></li>
<li><a href="/dashboard/<%= selectedGuild %>/mobs" class="block p-2 rounded-md hover:bg-white/5">Mobs</a></li>
</ul>
</nav>
</aside>
<!-- Mobile drawer/backdrop -->
<div id="drawerBackdrop" class="fixed inset-0 bg-black/40 z-40 hidden"></div>
<div id="mobileDrawer" class="fixed left-0 top-0 bottom-0 w-64 bg-white/6 backdrop-blur z-50 transform -translate-x-full transition-transform duration-300">
<div class="p-4">
<h4 class="text-white font-semibold mb-3">Servidor: <%= selectedGuildName || selectedGuild %></h4>
<ul class="flex flex-col gap-3">
<li><a href="/dashboard/<%= selectedGuild %>/overview" class="block p-2 rounded-md hover:bg-white/5">Overview</a></li>
<li><a href="/dashboard/<%= selectedGuild %>/members" class="block p-2 rounded-md hover:bg-white/5">Miembros</a></li>
<li><a href="/dashboard/<%= selectedGuild %>/settings" class="block p-2 rounded-md hover:bg-white/5">Ajustes</a></li>
<li><a href="/dashboard/<%= selectedGuild %>/areas" class="block p-2 rounded-md hover:bg-white/5">Game Areas</a></li>
<li><a href="/dashboard/<%= selectedGuild %>/mobs" class="block p-2 rounded-md hover:bg-white/5">Mobs</a></li>
</ul>
</div>
</div>
<% } %>
<div id="dashContent">
<% if (typeof page !== 'undefined' && page === 'overview' && selectedGuild) { %>
<div class="w-full max-w-7xl mx-auto mt-6 px-4">
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6">
<!-- Left sidebar -->
<aside class="lg:col-span-3">
<div class="bg-white/4 border border-white/6 rounded-xl p-4 sticky top-20">
<h3 class="text-slate-200 font-semibold mb-3">Menú</h3>
<ul class="space-y-2 text-slate-200">
<li><a href="/dashboard/<%= selectedGuild %>/overview" class="block p-2 rounded hover:bg-white/5">Overview</a></li>
<li><a href="/dashboard/<%= selectedGuild %>/members" class="block p-2 rounded hover:bg-white/5">Miembros</a></li>
<li><a href="/dashboard/<%= selectedGuild %>/settings" class="block p-2 rounded hover:bg-white/5">Ajustes</a></li>
<li><a href="/dashboard/<%= selectedGuild %>/areas" class="block p-2 rounded hover:bg-white/5">Game Areas</a></li>
<li><a href="/dashboard/<%= selectedGuild %>/mobs" class="block p-2 rounded hover:bg-white/5">Mobs</a></li>
</ul>
</div>
</aside>
<!-- Main content -->
<main class="lg:col-span-6">
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-3xl font-bold text-white">Welcome <span class="text-indigo-400"><%= user?.username || 'Admin' %></span></h1>
<p class="text-slate-300">find commonly used dashboard pages below.</p>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="bg-white/6 rounded-xl p-4">
<h4 class="text-white font-semibold">Custom messages</h4>
<p class="text-slate-300 text-sm mt-2">Create fully customized messages called templates and pack them with your very own embeds, buttons and select menus.</p>
<div class="mt-3"><a href="#" class="inline-block pixel-btn">Create template</a></div>
</div>
<div class="bg-white/6 rounded-xl p-4">
<h4 class="text-white font-semibold">Moderation cases</h4>
<p class="text-slate-300 text-sm mt-2">View and edit all moderation cases using the dashboard.</p>
<div class="mt-3"><a href="#" class="inline-block pixel-btn">View cases</a></div>
</div>
<div class="bg-white/6 rounded-xl p-4">
<h4 class="text-white font-semibold">User reports</h4>
<p class="text-slate-300 text-sm mt-2">Allow users to report others and fully customize how to handle them.</p>
<div class="mt-3"><a href="#" class="inline-block pixel-btn">Configure reports</a></div>
</div>
<div class="bg-white/6 rounded-xl p-4">
<h4 class="text-white font-semibold">Role greetings</h4>
<p class="text-slate-300 text-sm mt-2">Welcome users to their new role by using role assignment messages.</p>
<div class="mt-3"><a href="#" class="inline-block pixel-btn">Show role messages</a></div>
</div>
</div>
</main>
<!-- Right column (info / notices) -->
<aside class="lg:col-span-3">
<div class="bg-white/4 border border-white/6 rounded-xl p-4 sticky top-20">
<h4 class="text-white font-semibold mb-2">Notices</h4>
<div class="text-slate-300 text-sm">You are not in the support server. It is recommended to join for updates and support.</div>
</div>
</aside>
</div>
</div>
<% } %>
<% if (typeof page !== 'undefined' && page === 'settings' && selectedGuild) { %>
<div class="w-full max-w-3xl mt-6">
<div class="backdrop-blur-md bg-white/6 border border-white/8 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>
<a href="/dashboard/<%= selectedGuild %>/overview" class="text-sm text-slate-200/80 hover:underline">← Volver al overview</a>
</div>
<form id="guildSettingsForm" class="space-y-4">
<div>
<label class="block text-sm text-slate-200 mb-1">Prefix del bot</label>
<input type="text" name="prefix" id="prefixInput" value="<%= (guildConfig && guildConfig.prefix) || '' %>" class="w-full rounded p-2 bg-transparent border border-white/6" placeholder="!" />
</div>
<div>
<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>
<% 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>
<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" />
</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>
<div class="flex items-center gap-3">
<button type="submit" class="pixel-btn">Guardar</button>
<span id="saveStatus" class="text-sm text-slate-300"></span>
</div>
</form>
</div>
</div>
<script>
(function(){
const form = document.getElementById('guildSettingsForm');
const status = document.getElementById('saveStatus');
const staffFilter = document.getElementById('staffFilter');
if (staffFilter) {
staffFilter.addEventListener('input', ()=>{
const q = staffFilter.value.trim().toLowerCase();
const sel = document.getElementById('staffSelect');
if (!sel) return;
for (const opt of Array.from(sel.options)) {
const txt = (opt.textContent || '').toLowerCase();
opt.style.display = (!q || txt.indexOf(q) !== -1) ? '' : 'none';
}
});
}
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 = [];
const staffSelect = document.getElementById('staffSelect');
if (staffSelect) {
for (const o of staffSelect.selectedOptions) staffArr.push(o.value);
} else {
const staffRaw = (document.getElementById('staffInput') && document.getElementById('staffInput').value) ? 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>
<% } %>
</div>
<script>
// Simple fragment navigation for a SPA-like feel inside /dashboard
(function(){
async function loadFragment(href, push=true) {
try {
const url = new URL(href, location.origin);
url.searchParams.set('fragment','1');
const res = await fetch(url.toString(), { headers: { 'X-Requested-With':'XMLHttpRequest' } });
if (!res.ok) throw new Error('fetch-failed');
const html = await res.text();
const container = document.getElementById('dashContent');
if (!container) return;
container.innerHTML = html;
// execute inline scripts inside fragment
Array.from(container.querySelectorAll('script')).forEach(s=>{
try {
const n = document.createElement('script');
if (s.src) { n.src = s.src; n.async = false; document.head.appendChild(n); }
else { n.textContent = s.textContent; document.head.appendChild(n); document.head.removeChild(n); }
} catch(e){}
});
if (push) history.pushState({ href: href }, '', href);
} catch (err) {
console.warn('Fragment load failed', err);
location.href = href; // fallback full navigation
}
}
document.addEventListener('click', (e)=>{
const a = e.target.closest && e.target.closest('a');
if (!a) return;
const href = a.getAttribute('href');
if (!href) return;
// intercept internal dashboard links
if (href.startsWith('/dashboard') || href.indexOf(location.origin + '/dashboard') === 0) {
e.preventDefault();
loadFragment(href);
}
});
window.addEventListener('popstate', (e)=>{
const href = (e.state && e.state.href) || location.pathname + location.search;
loadFragment(href, false);
});
})();
</script>
</div>
</div>
<script>
// Drawer toggle logic
const drawerToggle = document.getElementById('drawerToggle');
const mobileDrawer = document.getElementById('mobileDrawer');
const drawerBackdrop = document.getElementById('drawerBackdrop');
function openDrawer() {
mobileDrawer.classList.remove('-translate-x-full');
drawerBackdrop.classList.remove('hidden');
}
function closeDrawer() {
mobileDrawer.classList.add('-translate-x-full');
drawerBackdrop.classList.add('hidden');
}
drawerToggle?.addEventListener('click', openDrawer);
drawerBackdrop?.addEventListener('click', closeDrawer);
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeDrawer();
});
</script>