Improve user lookup functionality with enhanced data retrieval and UI updates
Implement GET /user-lookup/:query endpoint to aggregate Supabase user data (profile, warnings, mod history), and update the dashboard UI for a better user lookup experience. Replit-Commit-Author: Agent Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: d918c1a9-0afe-4951-a804-0a3b0e38f04a Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/ocC7ZpF Replit-Helium-Checkpoint-Created: true
This commit is contained in:
parent
43394c8fe1
commit
aa291b3064
2 changed files with 446 additions and 5 deletions
|
|
@ -1565,6 +1565,179 @@ http
|
|||
return;
|
||||
}
|
||||
|
||||
// GET /user-lookup/:query - Search for a user by ID or username
|
||||
if (req.url.startsWith("/user-lookup/") && req.method === "GET") {
|
||||
const query = decodeURIComponent(req.url.split("/user-lookup/")[1].split("?")[0]);
|
||||
|
||||
if (!query || query.length < 2) {
|
||||
res.writeHead(400);
|
||||
res.end(JSON.stringify({ error: "Query too short" }));
|
||||
return;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const results = [];
|
||||
const isNumericId = /^\d{17,19}$/.test(query);
|
||||
|
||||
for (const guild of client.guilds.cache.values()) {
|
||||
try {
|
||||
if (isNumericId) {
|
||||
const member = await guild.members.fetch(query).catch(() => null);
|
||||
if (member) {
|
||||
const existingIdx = results.findIndex(r => r.id === member.id);
|
||||
if (existingIdx === -1) {
|
||||
results.push({
|
||||
id: member.id,
|
||||
tag: member.user.tag,
|
||||
username: member.user.username,
|
||||
displayName: member.displayName,
|
||||
avatar: member.user.displayAvatarURL({ size: 128 }),
|
||||
bot: member.user.bot,
|
||||
createdAt: member.user.createdAt.toISOString(),
|
||||
joinedAt: member.joinedAt?.toISOString(),
|
||||
roles: member.roles.cache.filter(r => r.name !== '@everyone').map(r => ({ id: r.id, name: r.name, color: r.hexColor })).slice(0, 10),
|
||||
servers: [{ id: guild.id, name: guild.name }],
|
||||
heat: getHeat(member.id),
|
||||
});
|
||||
} else {
|
||||
results[existingIdx].servers.push({ id: guild.id, name: guild.name });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const members = await guild.members.fetch({ query, limit: 10 }).catch(() => new Map());
|
||||
for (const member of members.values()) {
|
||||
const existingIdx = results.findIndex(r => r.id === member.id);
|
||||
if (existingIdx === -1) {
|
||||
results.push({
|
||||
id: member.id,
|
||||
tag: member.user.tag,
|
||||
username: member.user.username,
|
||||
displayName: member.displayName,
|
||||
avatar: member.user.displayAvatarURL({ size: 128 }),
|
||||
bot: member.user.bot,
|
||||
createdAt: member.user.createdAt.toISOString(),
|
||||
joinedAt: member.joinedAt?.toISOString(),
|
||||
roles: member.roles.cache.filter(r => r.name !== '@everyone').map(r => ({ id: r.id, name: r.name, color: r.hexColor })).slice(0, 10),
|
||||
servers: [{ id: guild.id, name: guild.name }],
|
||||
heat: getHeat(member.id),
|
||||
});
|
||||
} else {
|
||||
results[existingIdx].servers.push({ id: guild.id, name: guild.name });
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// Fetch Supabase profile data if available
|
||||
if (supabase && results.length > 0) {
|
||||
for (const user of results) {
|
||||
try {
|
||||
const { data: link } = await supabase
|
||||
.from('discord_links')
|
||||
.select('user_id, primary_arm')
|
||||
.eq('discord_id', user.id)
|
||||
.single();
|
||||
|
||||
if (link) {
|
||||
user.linked = true;
|
||||
user.realm = link.primary_arm;
|
||||
|
||||
const { data: profile } = await supabase
|
||||
.from('user_profiles')
|
||||
.select('username, xp, daily_streak, badges')
|
||||
.eq('id', link.user_id)
|
||||
.single();
|
||||
|
||||
if (profile) {
|
||||
user.aethexUsername = profile.username;
|
||||
user.xp = profile.xp || 0;
|
||||
user.level = Math.floor(Math.sqrt((profile.xp || 0) / 100));
|
||||
user.dailyStreak = profile.daily_streak || 0;
|
||||
user.badges = profile.badges || [];
|
||||
}
|
||||
}
|
||||
|
||||
const { data: warnings } = await supabase
|
||||
.from('warnings')
|
||||
.select('id, reason, created_at')
|
||||
.eq('user_id', user.id)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(5);
|
||||
|
||||
user.warnings = warnings || [];
|
||||
|
||||
const { data: modActions } = await supabase
|
||||
.from('mod_actions')
|
||||
.select('action, reason, created_at')
|
||||
.eq('user_id', user.id)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(5);
|
||||
|
||||
user.modHistory = modActions || [];
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
res.writeHead(200);
|
||||
res.end(JSON.stringify({
|
||||
results: results.slice(0, 20),
|
||||
count: results.length,
|
||||
query,
|
||||
timestamp: new Date().toISOString(),
|
||||
}));
|
||||
} catch (error) {
|
||||
res.writeHead(500);
|
||||
res.end(JSON.stringify({ error: error.message }));
|
||||
}
|
||||
})();
|
||||
return;
|
||||
}
|
||||
|
||||
// GET /mod-actions - Get recent moderation actions
|
||||
if (req.url === "/mod-actions" && req.method === "GET") {
|
||||
if (!supabase) {
|
||||
res.writeHead(200);
|
||||
res.end(JSON.stringify({ success: false, message: "Supabase not configured", actions: [] }));
|
||||
return;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const { data: actions, error } = await supabase
|
||||
.from('mod_actions')
|
||||
.select('*')
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(50);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
const { count: warnCount } = await supabase.from('warnings').select('*', { count: 'exact', head: true });
|
||||
const { count: banCount } = await supabase.from('mod_actions').select('*', { count: 'exact', head: true }).eq('action', 'ban');
|
||||
const { count: kickCount } = await supabase.from('mod_actions').select('*', { count: 'exact', head: true }).eq('action', 'kick');
|
||||
const { count: timeoutCount } = await supabase.from('mod_actions').select('*', { count: 'exact', head: true }).eq('action', 'timeout');
|
||||
|
||||
res.writeHead(200);
|
||||
res.end(JSON.stringify({
|
||||
success: true,
|
||||
actions: actions || [],
|
||||
counts: {
|
||||
warnings: warnCount || 0,
|
||||
bans: banCount || 0,
|
||||
kicks: kickCount || 0,
|
||||
timeouts: timeoutCount || 0,
|
||||
},
|
||||
timestamp: new Date().toISOString(),
|
||||
}));
|
||||
} catch (error) {
|
||||
res.writeHead(500);
|
||||
res.end(JSON.stringify({ success: false, error: error.message }));
|
||||
}
|
||||
})();
|
||||
return;
|
||||
}
|
||||
|
||||
res.writeHead(404);
|
||||
res.end(JSON.stringify({ error: "Not found" }));
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1548,20 +1548,37 @@
|
|||
<section class="section" id="section-users">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">User Lookup</h3>
|
||||
<h3 class="card-title">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
|
||||
User Lookup
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div style="max-width: 400px; margin-bottom: 1.5rem;">
|
||||
<input type="text" class="search-input" placeholder="Enter User ID or Username..." id="userSearch">
|
||||
<div style="display: flex; gap: 0.75rem; max-width: 500px; margin-bottom: 1.5rem;">
|
||||
<input type="text" class="search-input" placeholder="Enter User ID or Username..." id="userSearch" onkeypress="if(event.key==='Enter')searchUser()">
|
||||
<button class="btn btn-primary" onclick="searchUser()">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:16px;height:16px"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
|
||||
Search
|
||||
</button>
|
||||
</div>
|
||||
<div id="userSearchLoading" style="display: none;" class="loading"><div class="spinner"></div>Searching users...</div>
|
||||
<div id="userResult">
|
||||
<div class="empty-state">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
|
||||
<p>Search for a user to view their profile</p>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
||||
<p>Search for a user by Discord ID or username</p>
|
||||
<p style="font-size: 0.8rem; margin-top: 0.5rem;">View profile, XP, warnings, and mod history</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="userProfileCard" class="card" style="display: none;">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">User Profile</h3>
|
||||
<button class="btn btn-secondary btn-sm" onclick="clearUserSearch()">Clear</button>
|
||||
</div>
|
||||
<div class="card-body" id="userProfileContent"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Moderation Section -->
|
||||
|
|
@ -2771,6 +2788,257 @@
|
|||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// USER LOOKUP FUNCTIONS
|
||||
// ============================================
|
||||
|
||||
let currentUserResults = [];
|
||||
|
||||
async function searchUser() {
|
||||
const query = document.getElementById('userSearch').value.trim();
|
||||
if (!query || query.length < 2) {
|
||||
showToast('Please enter at least 2 characters', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('userSearchLoading').style.display = 'flex';
|
||||
document.getElementById('userResult').style.display = 'none';
|
||||
document.getElementById('userProfileCard').style.display = 'none';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/user-lookup/${encodeURIComponent(query)}`);
|
||||
const data = await response.json();
|
||||
currentUserResults = data.results || [];
|
||||
|
||||
document.getElementById('userSearchLoading').style.display = 'none';
|
||||
document.getElementById('userResult').style.display = 'block';
|
||||
|
||||
if (currentUserResults.length === 0) {
|
||||
document.getElementById('userResult').innerHTML = `
|
||||
<div class="empty-state">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
|
||||
<p>No users found for "${query}"</p>
|
||||
<p style="font-size: 0.8rem; margin-top: 0.5rem;">Try a Discord ID or different username</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentUserResults.length === 1) {
|
||||
showUserProfile(currentUserResults[0]);
|
||||
} else {
|
||||
document.getElementById('userResult').innerHTML = `
|
||||
<p style="margin-bottom: 1rem; color: var(--text-muted);">Found ${currentUserResults.length} users:</p>
|
||||
${currentUserResults.map((user, idx) => `
|
||||
<div class="server-item" style="cursor: pointer;" onclick="showUserProfile(currentUserResults[${idx}])">
|
||||
<div class="leaderboard-avatar">
|
||||
${user.avatar ? `<img src="${user.avatar}" alt="">` : user.username.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<div class="server-info">
|
||||
<div class="server-name">${user.username} ${user.bot ? '<span class="badge badge-info" style="margin-left: 0.5rem;">BOT</span>' : ''}</div>
|
||||
<div class="server-members" style="font-family: monospace;">${user.id}</div>
|
||||
</div>
|
||||
<span class="badge ${user.linked ? 'badge-success' : 'badge-warning'}">${user.linked ? 'Linked' : 'Not Linked'}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('User search failed:', error);
|
||||
document.getElementById('userSearchLoading').style.display = 'none';
|
||||
document.getElementById('userResult').style.display = 'block';
|
||||
document.getElementById('userResult').innerHTML = `
|
||||
<div class="empty-state">
|
||||
<p>Search failed. Please try again.</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function showUserProfile(user) {
|
||||
document.getElementById('userResult').innerHTML = `
|
||||
<div class="empty-state">
|
||||
<p>Profile shown below</p>
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('userProfileCard').style.display = 'block';
|
||||
|
||||
const heatClass = user.heat >= 3 ? 'danger' : user.heat >= 1 ? 'warning' : 'success';
|
||||
const rolesHtml = user.roles && user.roles.length > 0
|
||||
? user.roles.map(r => `<span class="badge" style="background: ${r.color}20; color: ${r.color}; margin-right: 0.25rem; margin-bottom: 0.25rem;">${r.name}</span>`).join('')
|
||||
: '<span class="badge badge-warning">No roles</span>';
|
||||
|
||||
const serversHtml = user.servers && user.servers.length > 0
|
||||
? user.servers.map(s => `<span class="badge badge-info" style="margin-right: 0.25rem;">${s.name}</span>`).join('')
|
||||
: 'None';
|
||||
|
||||
const warningsHtml = user.warnings && user.warnings.length > 0
|
||||
? user.warnings.map(w => `
|
||||
<div class="activity-item">
|
||||
<div class="activity-icon stat-icon orange">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/></svg>
|
||||
</div>
|
||||
<div class="activity-content">
|
||||
<div class="activity-text">${w.reason || 'No reason provided'}</div>
|
||||
<div class="activity-time">${formatTimeAgo(w.created_at)}</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')
|
||||
: '<p style="color: var(--text-muted);">No warnings</p>';
|
||||
|
||||
const modHistoryHtml = user.modHistory && user.modHistory.length > 0
|
||||
? user.modHistory.map(m => `
|
||||
<div class="activity-item">
|
||||
<div class="activity-icon stat-icon ${m.action === 'ban' ? 'red' : m.action === 'kick' ? 'purple' : 'blue'}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
||||
</div>
|
||||
<div class="activity-content">
|
||||
<div class="activity-text"><strong>${m.action.toUpperCase()}</strong> - ${m.reason || 'No reason'}</div>
|
||||
<div class="activity-time">${formatTimeAgo(m.created_at)}</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')
|
||||
: '<p style="color: var(--text-muted);">No mod actions</p>';
|
||||
|
||||
document.getElementById('userProfileContent').innerHTML = `
|
||||
<div class="grid-2">
|
||||
<div>
|
||||
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 1.5rem;">
|
||||
<div class="leaderboard-avatar" style="width: 80px; height: 80px; font-size: 2rem;">
|
||||
${user.avatar ? `<img src="${user.avatar}" alt="">` : user.username.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<div>
|
||||
<h2 style="margin-bottom: 0.25rem;">${user.displayName || user.username}</h2>
|
||||
<p style="color: var(--text-muted); font-size: 0.9rem;">@${user.username} ${user.bot ? '<span class="badge badge-info">BOT</span>' : ''}</p>
|
||||
<p style="font-family: monospace; font-size: 0.8rem; color: var(--text-muted);">${user.id}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid" style="grid-template-columns: repeat(3, 1fr); margin-bottom: 1.5rem;">
|
||||
<div style="text-align: center; padding: 0.75rem; background: var(--bg-card); border-radius: 8px;">
|
||||
<div style="font-size: 1.5rem; font-weight: 700; color: var(--accent);">${user.level || 0}</div>
|
||||
<div style="font-size: 0.75rem; color: var(--text-muted);">Level</div>
|
||||
</div>
|
||||
<div style="text-align: center; padding: 0.75rem; background: var(--bg-card); border-radius: 8px;">
|
||||
<div style="font-size: 1.5rem; font-weight: 700; color: var(--success);">${formatNumber(user.xp || 0)}</div>
|
||||
<div style="font-size: 0.75rem; color: var(--text-muted);">XP</div>
|
||||
</div>
|
||||
<div style="text-align: center; padding: 0.75rem; background: var(--bg-card); border-radius: 8px;">
|
||||
<div style="font-size: 1.5rem; font-weight: 700; color: var(--${heatClass});">${user.heat || 0}</div>
|
||||
<div style="font-size: 0.75rem; color: var(--text-muted);">Heat</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<h4 style="margin-bottom: 0.5rem;">Account Status</h4>
|
||||
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
||||
<span class="badge ${user.linked ? 'badge-success' : 'badge-warning'}">${user.linked ? 'AeThex Linked' : 'Not Linked'}</span>
|
||||
${user.realm ? `<span class="badge badge-info">${user.realm}</span>` : ''}
|
||||
${user.dailyStreak ? `<span class="badge badge-success">${user.dailyStreak} day streak</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<h4 style="margin-bottom: 0.5rem;">Servers</h4>
|
||||
<div style="display: flex; gap: 0.25rem; flex-wrap: wrap;">${serversHtml}</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<h4 style="margin-bottom: 0.5rem;">Roles</h4>
|
||||
<div style="display: flex; gap: 0.25rem; flex-wrap: wrap;">${rolesHtml}</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<h4 style="margin-bottom: 0.5rem;">Dates</h4>
|
||||
<p style="font-size: 0.875rem;"><strong>Created:</strong> ${new Date(user.createdAt).toLocaleDateString()}</p>
|
||||
${user.joinedAt ? `<p style="font-size: 0.875rem;"><strong>Joined:</strong> ${new Date(user.joinedAt).toLocaleDateString()}</p>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="card" style="margin-bottom: 1rem;">
|
||||
<div class="card-header">
|
||||
<h4 class="card-title" style="font-size: 1rem;">Warnings (${user.warnings?.length || 0})</h4>
|
||||
</div>
|
||||
<div class="card-body" style="max-height: 200px; overflow-y: auto;">
|
||||
${warningsHtml}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4 class="card-title" style="font-size: 1rem;">Mod History</h4>
|
||||
</div>
|
||||
<div class="card-body" style="max-height: 200px; overflow-y: auto;">
|
||||
${modHistoryHtml}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 1rem;">
|
||||
<h4 style="margin-bottom: 0.5rem;">Quick Actions</h4>
|
||||
<p style="color: var(--text-muted); font-size: 0.875rem;">Use Discord commands for moderation:</p>
|
||||
<div class="commands-grid" style="margin-top: 0.5rem; grid-template-columns: repeat(2, 1fr);">
|
||||
<div class="command-item">warn ${user.id}</div>
|
||||
<div class="command-item">kick ${user.id}</div>
|
||||
<div class="command-item">timeout ${user.id}</div>
|
||||
<div class="command-item">ban ${user.id}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function clearUserSearch() {
|
||||
document.getElementById('userSearch').value = '';
|
||||
document.getElementById('userProfileCard').style.display = 'none';
|
||||
document.getElementById('userResult').innerHTML = `
|
||||
<div class="empty-state">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
||||
<p>Search for a user by Discord ID or username</p>
|
||||
<p style="font-size: 0.8rem; margin-top: 0.5rem;">View profile, XP, warnings, and mod history</p>
|
||||
</div>
|
||||
`;
|
||||
currentUserResults = [];
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// MODERATION FETCH
|
||||
// ============================================
|
||||
|
||||
async function fetchModActions() {
|
||||
try {
|
||||
const response = await fetch('/mod-actions');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.counts) {
|
||||
document.getElementById('warningCount').textContent = data.counts.warnings || 0;
|
||||
document.getElementById('banCount').textContent = data.counts.bans || 0;
|
||||
document.getElementById('timeoutCount').textContent = data.counts.timeouts || 0;
|
||||
document.getElementById('kickCount').textContent = data.counts.kicks || 0;
|
||||
}
|
||||
|
||||
const modLogContent = document.getElementById('modLogContent');
|
||||
if (data.actions && data.actions.length > 0) {
|
||||
modLogContent.innerHTML = data.actions.slice(0, 10).map(a => `
|
||||
<div class="activity-item">
|
||||
<div class="activity-icon stat-icon ${a.action === 'ban' ? 'red' : a.action === 'kick' ? 'purple' : a.action === 'warn' ? 'orange' : 'blue'}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
||||
</div>
|
||||
<div class="activity-content">
|
||||
<div class="activity-text"><strong>${a.action.toUpperCase()}</strong> ${a.user_tag || a.user_id} - ${a.reason || 'No reason'}</div>
|
||||
<div class="activity-time">by ${a.moderator_tag || 'Unknown'} ${formatTimeAgo(a.created_at)}</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} else {
|
||||
modLogContent.innerHTML = '<div class="empty-state"><p>No recent moderation actions</p></div>';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch mod actions:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function showToast(message, type = 'success') {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast ${type}`;
|
||||
|
|
|
|||
Loading…
Reference in a new issue