Replaced `.single()` with `.maybeSingle()` in multiple command files to handle cases where no record is found, and added a new /pricing route and navigation links to the UI. Replit-Commit-Author: Agent Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: e91d020a-35a6-4add-9945-887dd3ecae9f Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/tdDjujk Replit-Helium-Checkpoint-Created: true
833 lines
25 KiB
HTML
833 lines
25 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Federation - AeThex | Warden</title>
|
|
<style>
|
|
:root {
|
|
--background: #030712;
|
|
--foreground: #f8fafc;
|
|
--card: rgba(15, 23, 42, 0.6);
|
|
--card-border: rgba(99, 102, 241, 0.15);
|
|
--card-border-hover: rgba(99, 102, 241, 0.4);
|
|
--primary: #6366f1;
|
|
--primary-light: #818cf8;
|
|
--secondary: rgba(30, 41, 59, 0.5);
|
|
--muted: #64748b;
|
|
--border: rgba(51, 65, 85, 0.5);
|
|
--success: #10b981;
|
|
--warning: #f59e0b;
|
|
--danger: #ef4444;
|
|
}
|
|
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
body {
|
|
font-family: 'Courier New', Courier, monospace;
|
|
background: var(--background);
|
|
color: var(--foreground);
|
|
min-height: 100vh;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.bg-grid {
|
|
position: fixed;
|
|
inset: 0;
|
|
background-image:
|
|
linear-gradient(rgba(99, 102, 241, 0.03) 1px, transparent 1px),
|
|
linear-gradient(90deg, rgba(99, 102, 241, 0.03) 1px, transparent 1px);
|
|
background-size: 64px 64px;
|
|
pointer-events: none;
|
|
z-index: -2;
|
|
}
|
|
|
|
.container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 0 1.5rem;
|
|
}
|
|
|
|
header {
|
|
background: rgba(3, 7, 18, 0.8);
|
|
backdrop-filter: blur(20px);
|
|
border-bottom: 1px solid var(--border);
|
|
padding: 1rem 0;
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 100;
|
|
}
|
|
|
|
.header-content {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.logo {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
text-decoration: none;
|
|
color: var(--foreground);
|
|
}
|
|
|
|
.logo-icon { width: 40px; height: 40px; border-radius: 8px; }
|
|
.logo-text { font-size: 1.25rem; font-weight: 700; }
|
|
|
|
.nav-links {
|
|
display: flex;
|
|
gap: 2rem;
|
|
align-items: center;
|
|
}
|
|
|
|
.nav-links a {
|
|
color: var(--muted);
|
|
text-decoration: none;
|
|
font-weight: 500;
|
|
transition: color 0.2s;
|
|
}
|
|
|
|
.nav-links a:hover, .nav-links a.active { color: var(--foreground); }
|
|
|
|
.btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.625rem 1.25rem;
|
|
border-radius: 8px;
|
|
font-weight: 600;
|
|
text-decoration: none;
|
|
transition: all 0.2s;
|
|
border: none;
|
|
cursor: pointer;
|
|
font-size: 0.875rem;
|
|
font-family: inherit;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: linear-gradient(135deg, var(--primary), #3b82f6);
|
|
color: white;
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: var(--secondary);
|
|
border: 1px solid var(--border);
|
|
color: var(--foreground);
|
|
}
|
|
|
|
.btn-success { background: var(--success); color: white; }
|
|
.btn-danger { background: var(--danger); color: white; }
|
|
.btn-sm { padding: 0.375rem 0.75rem; font-size: 0.8rem; }
|
|
|
|
.hero {
|
|
padding: 3rem 0;
|
|
text-align: center;
|
|
}
|
|
|
|
.hero h1 {
|
|
font-size: 2.5rem;
|
|
font-weight: 700;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.hero p {
|
|
color: var(--muted);
|
|
font-size: 1.1rem;
|
|
}
|
|
|
|
.text-gradient {
|
|
background: linear-gradient(135deg, var(--primary), #3b82f6, #06b6d4);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
}
|
|
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 1.5rem;
|
|
margin-bottom: 3rem;
|
|
}
|
|
|
|
.stat-card {
|
|
background: var(--card);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 16px;
|
|
padding: 1.5rem;
|
|
text-align: center;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 2.5rem;
|
|
font-weight: 700;
|
|
background: linear-gradient(135deg, var(--primary), #3b82f6);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
}
|
|
|
|
.stat-label {
|
|
color: var(--muted);
|
|
font-size: 0.9rem;
|
|
margin-top: 0.25rem;
|
|
}
|
|
|
|
.tabs {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
margin-bottom: 2rem;
|
|
border-bottom: 1px solid var(--border);
|
|
padding-bottom: 1rem;
|
|
}
|
|
|
|
.tab {
|
|
padding: 0.75rem 1.5rem;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
background: transparent;
|
|
border: 1px solid transparent;
|
|
color: var(--muted);
|
|
font-family: inherit;
|
|
font-weight: 500;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.tab:hover { color: var(--foreground); }
|
|
|
|
.tab.active {
|
|
background: var(--card);
|
|
border-color: var(--primary);
|
|
color: var(--foreground);
|
|
}
|
|
|
|
.section { display: none; }
|
|
.section.active { display: block; }
|
|
|
|
.card {
|
|
background: var(--card);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 16px;
|
|
padding: 1.5rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.card-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.card-title { font-size: 1.1rem; font-weight: 600; }
|
|
|
|
.table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
.table th, .table td {
|
|
padding: 0.75rem;
|
|
text-align: left;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.table th {
|
|
color: var(--muted);
|
|
font-size: 0.85rem;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.badge {
|
|
display: inline-block;
|
|
padding: 0.25rem 0.5rem;
|
|
border-radius: 4px;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.badge-low { background: rgba(255, 255, 0, 0.2); color: #ffd700; }
|
|
.badge-medium { background: rgba(255, 153, 0, 0.2); color: #ff9900; }
|
|
.badge-high { background: rgba(255, 51, 0, 0.2); color: #ff3300; }
|
|
.badge-critical { background: rgba(255, 0, 0, 0.2); color: #ff0000; }
|
|
.badge-pending { background: rgba(255, 193, 7, 0.2); color: var(--warning); }
|
|
.badge-approved { background: rgba(16, 185, 129, 0.2); color: var(--success); }
|
|
|
|
.server-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
gap: 1rem;
|
|
}
|
|
|
|
.server-card {
|
|
background: var(--card);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 12px;
|
|
padding: 1.25rem;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.server-card:hover {
|
|
border-color: var(--card-border-hover);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.server-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
margin-bottom: 0.75rem;
|
|
}
|
|
|
|
.server-icon {
|
|
width: 48px;
|
|
height: 48px;
|
|
border-radius: 12px;
|
|
background: var(--secondary);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 1.5rem;
|
|
}
|
|
|
|
.server-name { font-weight: 600; }
|
|
.server-category { font-size: 0.8rem; color: var(--muted); }
|
|
.server-desc { font-size: 0.9rem; color: var(--muted); margin-bottom: 0.75rem; }
|
|
.server-members { font-size: 0.85rem; color: var(--primary-light); }
|
|
|
|
.leaderboard-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
padding: 1rem;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.leaderboard-rank {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: 700;
|
|
background: var(--secondary);
|
|
}
|
|
|
|
.leaderboard-rank.gold { background: linear-gradient(135deg, #ffd700, #ff8c00); color: #000; }
|
|
.leaderboard-rank.silver { background: linear-gradient(135deg, #c0c0c0, #a0a0a0); color: #000; }
|
|
.leaderboard-rank.bronze { background: linear-gradient(135deg, #cd7f32, #8b4513); color: #fff; }
|
|
|
|
.leaderboard-info { flex: 1; }
|
|
.leaderboard-name { font-weight: 600; }
|
|
.leaderboard-tier { font-size: 0.8rem; color: var(--muted); }
|
|
.leaderboard-score { font-weight: 700; color: var(--primary-light); }
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 3rem;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.pricing-section {
|
|
margin-bottom: 3rem;
|
|
}
|
|
|
|
.pricing-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
gap: 1.5rem;
|
|
}
|
|
|
|
.pricing-card {
|
|
background: var(--card);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 16px;
|
|
padding: 2rem;
|
|
text-align: center;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.pricing-card:hover {
|
|
border-color: var(--card-border-hover);
|
|
transform: translateY(-4px);
|
|
}
|
|
|
|
.pricing-card.featured {
|
|
border-color: var(--primary);
|
|
background: linear-gradient(180deg, rgba(99, 102, 241, 0.1) 0%, var(--card) 100%);
|
|
}
|
|
|
|
.pricing-badge {
|
|
display: inline-block;
|
|
padding: 0.25rem 0.75rem;
|
|
background: var(--primary);
|
|
color: white;
|
|
border-radius: 20px;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.pricing-title { font-size: 1.5rem; font-weight: 700; margin-bottom: 0.5rem; }
|
|
.pricing-price { font-size: 2.5rem; font-weight: 700; color: var(--primary-light); }
|
|
.pricing-period { font-size: 0.9rem; color: var(--muted); }
|
|
.pricing-desc { color: var(--muted); margin: 1rem 0; font-size: 0.9rem; }
|
|
|
|
.pricing-features {
|
|
list-style: none;
|
|
text-align: left;
|
|
margin: 1.5rem 0;
|
|
}
|
|
|
|
.pricing-features li {
|
|
padding: 0.5rem 0;
|
|
color: var(--muted);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.pricing-features li::before {
|
|
content: '✓';
|
|
color: var(--success);
|
|
font-weight: 700;
|
|
}
|
|
|
|
.select-wrapper {
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.select-wrapper select {
|
|
width: 100%;
|
|
padding: 0.75rem;
|
|
border-radius: 8px;
|
|
background: var(--secondary);
|
|
border: 1px solid var(--border);
|
|
color: var(--foreground);
|
|
font-family: inherit;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.stats-grid { grid-template-columns: 1fr; }
|
|
.nav-links { display: none; }
|
|
.tabs { overflow-x: auto; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="bg-grid"></div>
|
|
|
|
<header>
|
|
<div class="container header-content">
|
|
<a href="/" class="logo">
|
|
<img src="/logo.png" alt="AeThex" class="logo-icon">
|
|
<span class="logo-text">AeThex | Warden</span>
|
|
</a>
|
|
|
|
<nav class="nav-links">
|
|
<a href="/">Home</a>
|
|
<a href="/features">Features</a>
|
|
<a href="/commands">Commands</a>
|
|
<a href="/federation" class="active">Federation</a>
|
|
<a href="/pricing">Pricing</a>
|
|
</nav>
|
|
|
|
<a href="https://discord.com/api/oauth2/authorize?client_id=578971245454950421&permissions=8&scope=bot%20applications.commands" class="btn btn-primary" target="_blank">Add to Server</a>
|
|
</div>
|
|
</header>
|
|
|
|
<main>
|
|
<section class="hero">
|
|
<div class="container">
|
|
<h1>The <span class="text-gradient">Federation</span></h1>
|
|
<p>A network of protected servers. Ban one, ban all.</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="container pricing-section">
|
|
<h2 style="text-align: center; margin-bottom: 2rem;">Upgrade Your Protection</h2>
|
|
<div class="pricing-grid">
|
|
<div class="pricing-card">
|
|
<div class="pricing-title">Free</div>
|
|
<div class="pricing-price">$0</div>
|
|
<div class="pricing-period">forever</div>
|
|
<div class="pricing-desc">Basic protection for all federation members</div>
|
|
<ul class="pricing-features">
|
|
<li>Critical threat auto-bans</li>
|
|
<li>Server directory listing</li>
|
|
<li>Reputation tracking</li>
|
|
<li>Community support</li>
|
|
</ul>
|
|
<button class="btn btn-secondary" disabled>Current Plan</button>
|
|
</div>
|
|
|
|
<div class="pricing-card featured">
|
|
<div class="pricing-badge">RECOMMENDED</div>
|
|
<div class="pricing-title">Premium</div>
|
|
<div class="pricing-price">$50</div>
|
|
<div class="pricing-period">per month</div>
|
|
<div class="pricing-desc">Full protection from ALL threat levels</div>
|
|
<ul class="pricing-features">
|
|
<li>Auto-kick for ALL ban severities</li>
|
|
<li>Real-time threat alerts</li>
|
|
<li>Priority modlog notifications</li>
|
|
<li>Premium support badge</li>
|
|
</ul>
|
|
<div class="select-wrapper">
|
|
<select id="premiumGuildSelect">
|
|
<option value="">Select a server...</option>
|
|
</select>
|
|
</div>
|
|
<button class="btn btn-primary" onclick="upgradePlan('premium')">Upgrade to Premium</button>
|
|
</div>
|
|
|
|
<div class="pricing-card">
|
|
<div class="pricing-title">Featured Slot</div>
|
|
<div class="pricing-price">$200</div>
|
|
<div class="pricing-period">per week</div>
|
|
<div class="pricing-desc">Promote your server across the entire federation</div>
|
|
<ul class="pricing-features">
|
|
<li>Featured in all member servers</li>
|
|
<li>Cross-server promotion</li>
|
|
<li>Boost your member count</li>
|
|
<li>Priority directory placement</li>
|
|
</ul>
|
|
<div class="select-wrapper">
|
|
<select id="featuredGuildSelect">
|
|
<option value="">Select a server...</option>
|
|
</select>
|
|
</div>
|
|
<button class="btn btn-primary" onclick="upgradePlan('featured')">Get Featured</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="container">
|
|
<div class="stats-grid">
|
|
<div class="stat-card">
|
|
<div class="stat-value" id="totalServers">-</div>
|
|
<div class="stat-label">Member Servers</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value" id="activeBans">-</div>
|
|
<div class="stat-label">Active Bans</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value" id="pendingApps">-</div>
|
|
<div class="stat-label">Pending Applications</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tabs">
|
|
<button class="tab active" data-tab="servers">Servers</button>
|
|
<button class="tab" data-tab="bans">Global Bans</button>
|
|
<button class="tab" data-tab="applications">Applications</button>
|
|
<button class="tab" data-tab="leaderboard">Leaderboard</button>
|
|
</div>
|
|
|
|
<div id="servers" class="section active">
|
|
<div class="server-grid" id="serverGrid">
|
|
<div class="empty-state">Loading servers...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="bans" class="section">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<span class="card-title">Global Ban List</span>
|
|
</div>
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>User</th>
|
|
<th>Severity</th>
|
|
<th>Reason</th>
|
|
<th>Date</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="banList">
|
|
<tr><td colspan="4" class="empty-state">Loading bans...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="applications" class="section">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<span class="card-title">Pending Applications</span>
|
|
</div>
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Server</th>
|
|
<th>Category</th>
|
|
<th>Members</th>
|
|
<th>Status</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="appList">
|
|
<tr><td colspan="5" class="empty-state">Loading applications...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="leaderboard" class="section">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<span class="card-title">Federation Reputation Leaders</span>
|
|
</div>
|
|
<div id="leaderboardList">
|
|
<div class="empty-state">Loading leaderboard...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
|
|
<script>
|
|
document.querySelectorAll('.tab').forEach(tab => {
|
|
tab.addEventListener('click', () => {
|
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
document.querySelectorAll('.section').forEach(s => s.classList.remove('active'));
|
|
tab.classList.add('active');
|
|
document.getElementById(tab.dataset.tab).classList.add('active');
|
|
});
|
|
});
|
|
|
|
async function loadStats() {
|
|
try {
|
|
const res = await fetch('/api/federation/stats');
|
|
const data = await res.json();
|
|
document.getElementById('totalServers').textContent = data.totalServers || 0;
|
|
document.getElementById('activeBans').textContent = data.activeBans || 0;
|
|
document.getElementById('pendingApps').textContent = data.pendingApplications || 0;
|
|
} catch (e) {
|
|
console.error('Failed to load stats:', e);
|
|
}
|
|
}
|
|
|
|
async function loadServers() {
|
|
try {
|
|
const res = await fetch('/api/federation/servers');
|
|
const data = await res.json();
|
|
const grid = document.getElementById('serverGrid');
|
|
|
|
if (!data.servers || data.servers.length === 0) {
|
|
grid.innerHTML = '<div class="empty-state">No servers in the federation yet. Use /federation membership apply to join!</div>';
|
|
return;
|
|
}
|
|
|
|
const categoryEmojis = { gaming: '🎮', creative: '🎨', development: '💻', education: '📚', community: '👥', business: '🏢' };
|
|
|
|
grid.innerHTML = data.servers.map(s => `
|
|
<div class="server-card">
|
|
<div class="server-header">
|
|
<div class="server-icon">${categoryEmojis[s.category] || '🌐'}</div>
|
|
<div>
|
|
<div class="server-name">${s.guild_name}</div>
|
|
<div class="server-category">${s.category || 'General'}</div>
|
|
</div>
|
|
</div>
|
|
<div class="server-desc">${s.description || 'No description'}</div>
|
|
<div class="server-members">${(s.member_count || 0).toLocaleString()} members</div>
|
|
</div>
|
|
`).join('');
|
|
} catch (e) {
|
|
console.error('Failed to load servers:', e);
|
|
}
|
|
}
|
|
|
|
async function loadBans() {
|
|
try {
|
|
const res = await fetch('/api/federation/bans?limit=50');
|
|
const data = await res.json();
|
|
const tbody = document.getElementById('banList');
|
|
|
|
if (!data.bans || data.bans.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="4" class="empty-state">No active bans</td></tr>';
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = data.bans.map(b => `
|
|
<tr>
|
|
<td>${b.username || b.user_id}</td>
|
|
<td><span class="badge badge-${b.severity}">${b.severity.toUpperCase()}</span></td>
|
|
<td>${b.reason.substring(0, 50)}${b.reason.length > 50 ? '...' : ''}</td>
|
|
<td>${new Date(b.created_at).toLocaleDateString()}</td>
|
|
</tr>
|
|
`).join('');
|
|
} catch (e) {
|
|
console.error('Failed to load bans:', e);
|
|
}
|
|
}
|
|
|
|
async function loadApplications() {
|
|
try {
|
|
const res = await fetch('/api/federation/applications');
|
|
const data = await res.json();
|
|
const tbody = document.getElementById('appList');
|
|
|
|
if (!data.applications || data.applications.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="5" class="empty-state">No applications</td></tr>';
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = data.applications.map(a => `
|
|
<tr>
|
|
<td>${a.guild_name}</td>
|
|
<td>${a.category || 'General'}</td>
|
|
<td>${(a.member_count || 0).toLocaleString()}</td>
|
|
<td><span class="badge badge-${a.status}">${a.status.toUpperCase()}</span></td>
|
|
<td>
|
|
${a.status === 'pending' ? `
|
|
<button class="btn btn-success btn-sm" onclick="approveApp(${a.id})">Approve</button>
|
|
<button class="btn btn-danger btn-sm" onclick="rejectApp(${a.id})">Reject</button>
|
|
` : '-'}
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
} catch (e) {
|
|
console.error('Failed to load applications:', e);
|
|
}
|
|
}
|
|
|
|
async function loadLeaderboard() {
|
|
try {
|
|
const res = await fetch('/api/federation/leaderboard?limit=20');
|
|
const data = await res.json();
|
|
const container = document.getElementById('leaderboardList');
|
|
|
|
if (!data.leaderboard || data.leaderboard.length === 0) {
|
|
container.innerHTML = '<div class="empty-state">No reputation data yet. Be active across federation servers!</div>';
|
|
return;
|
|
}
|
|
|
|
const tierEmojis = { newcomer: '🌱', member: '⭐', veteran: '🏆', elite: '💎', legend: '👑' };
|
|
|
|
container.innerHTML = data.leaderboard.map((l, i) => {
|
|
const rankClass = i === 0 ? 'gold' : i === 1 ? 'silver' : i === 2 ? 'bronze' : '';
|
|
return `
|
|
<div class="leaderboard-item">
|
|
<div class="leaderboard-rank ${rankClass}">${i + 1}</div>
|
|
<div class="leaderboard-info">
|
|
<div class="leaderboard-name">${l.discord_id}</div>
|
|
<div class="leaderboard-tier">${tierEmojis[l.rank_tier] || '🌱'} ${(l.rank_tier || 'newcomer').toUpperCase()}</div>
|
|
</div>
|
|
<div class="leaderboard-score">${(l.reputation_score || 0).toLocaleString()} rep</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
} catch (e) {
|
|
console.error('Failed to load leaderboard:', e);
|
|
}
|
|
}
|
|
|
|
async function approveApp(id) {
|
|
if (!confirm('Approve this application?')) return;
|
|
try {
|
|
await fetch(`/api/federation/applications/${id}/approve`, { method: 'POST' });
|
|
loadApplications();
|
|
loadStats();
|
|
loadServers();
|
|
} catch (e) {
|
|
alert('Failed to approve');
|
|
}
|
|
}
|
|
|
|
async function rejectApp(id) {
|
|
const reason = prompt('Rejection reason:');
|
|
if (!reason) return;
|
|
try {
|
|
await fetch(`/api/federation/applications/${id}/reject`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ reason })
|
|
});
|
|
loadApplications();
|
|
loadStats();
|
|
} catch (e) {
|
|
alert('Failed to reject');
|
|
}
|
|
}
|
|
|
|
async function loadUserGuilds() {
|
|
try {
|
|
const res = await fetch('/api/user');
|
|
if (!res.ok) return;
|
|
|
|
const data = await res.json();
|
|
if (!data.user?.guilds) return;
|
|
|
|
const adminGuilds = data.user.guilds.filter(g => g.isAdmin);
|
|
|
|
const premiumSelect = document.getElementById('premiumGuildSelect');
|
|
const featuredSelect = document.getElementById('featuredGuildSelect');
|
|
|
|
adminGuilds.forEach(g => {
|
|
const option1 = new Option(g.name, g.id);
|
|
const option2 = new Option(g.name, g.id);
|
|
premiumSelect.appendChild(option1);
|
|
featuredSelect.appendChild(option2);
|
|
});
|
|
} catch (e) {
|
|
console.error('Failed to load user guilds:', e);
|
|
}
|
|
}
|
|
|
|
async function upgradePlan(planType) {
|
|
const selectId = planType === 'premium' ? 'premiumGuildSelect' : 'featuredGuildSelect';
|
|
const guildId = document.getElementById(selectId).value;
|
|
|
|
if (!guildId) {
|
|
alert('Please select a server first');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const res = await fetch('/api/stripe/create-checkout', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ guildId, planType })
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const error = await res.json();
|
|
if (res.status === 401) {
|
|
alert('Please log in first using Discord');
|
|
window.location.href = '/auth/discord';
|
|
return;
|
|
}
|
|
throw new Error(error.error || 'Failed to create checkout');
|
|
}
|
|
|
|
const data = await res.json();
|
|
if (data.url) {
|
|
window.location.href = data.url;
|
|
}
|
|
} catch (e) {
|
|
alert('Failed to start checkout: ' + e.message);
|
|
}
|
|
}
|
|
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
if (urlParams.get('success') === 'true') {
|
|
const plan = urlParams.get('plan');
|
|
alert(`Successfully upgraded to ${plan === 'premium' ? 'Premium' : 'Featured Slot'}! Your server is now protected.`);
|
|
window.history.replaceState({}, '', '/federation');
|
|
}
|
|
if (urlParams.get('canceled') === 'true') {
|
|
window.history.replaceState({}, '', '/federation');
|
|
}
|
|
|
|
loadStats();
|
|
loadServers();
|
|
loadBans();
|
|
loadApplications();
|
|
loadLeaderboard();
|
|
loadUserGuilds();
|
|
</script>
|
|
</body>
|
|
</html>
|