Introduces a mobile-friendly navigation system for the dashboard with a slide-out sidebar and overlay, alongside a new AI chat command utilizing the OpenAI API for image generation and conversational interactions. Replit-Commit-Author: Agent Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: a4c8a9aa-aa7e-4928-95e7-0c95279d5dc6 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/3tJ1Z1J Replit-Helium-Checkpoint-Created: true
4610 lines
172 KiB
HTML
4610 lines
172 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Dashboard - AeThex | Warden</title>
|
|
<meta name="description" content="Manage your Discord server with Warden's powerful dashboard. Configure XP settings, quests, achievements, moderation, and more.">
|
|
<meta name="robots" content="noindex, nofollow">
|
|
<meta name="theme-color" content="#6366f1">
|
|
<link rel="icon" href="/logo.png" type="image/png">
|
|
<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;
|
|
--muted-foreground: #94a3b8;
|
|
--border: rgba(51, 65, 85, 0.5);
|
|
--gradient-1: #6366f1;
|
|
--gradient-2: #3b82f6;
|
|
--gradient-3: #06b6d4;
|
|
--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;
|
|
overflow-x: hidden;
|
|
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;
|
|
}
|
|
|
|
.bg-glow {
|
|
position: fixed;
|
|
top: -50%;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
width: 150%;
|
|
height: 100%;
|
|
background: radial-gradient(ellipse at center, rgba(99, 102, 241, 0.12) 0%, transparent 60%);
|
|
pointer-events: none;
|
|
z-index: -1;
|
|
}
|
|
|
|
.app {
|
|
display: flex;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
.sidebar {
|
|
width: 280px;
|
|
background: rgba(3, 7, 18, 0.95);
|
|
backdrop-filter: blur(20px);
|
|
border-right: 1px solid var(--border);
|
|
padding: 1.5rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
position: fixed;
|
|
height: 100vh;
|
|
overflow-y: auto;
|
|
z-index: 50;
|
|
}
|
|
|
|
.logo {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
margin-bottom: 2rem;
|
|
text-decoration: none;
|
|
color: var(--foreground);
|
|
}
|
|
|
|
.logo-icon {
|
|
width: 44px;
|
|
height: 44px;
|
|
border-radius: 10px;
|
|
object-fit: contain;
|
|
}
|
|
|
|
.logo-text {
|
|
font-size: 1.25rem;
|
|
font-weight: 700;
|
|
background: linear-gradient(135deg, var(--foreground), var(--muted));
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
}
|
|
|
|
.guild-selector {
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.guild-selector-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 0.875rem 1rem;
|
|
background: var(--card);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 12px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
|
|
.guild-selector-header:hover {
|
|
border-color: var(--card-border-hover);
|
|
}
|
|
|
|
.guild-selector-header.open {
|
|
border-radius: 12px 12px 0 0;
|
|
border-bottom-color: transparent;
|
|
}
|
|
|
|
.selected-guild {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.selected-guild-icon {
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 8px;
|
|
background: linear-gradient(135deg, var(--gradient-1), var(--gradient-2));
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: 600;
|
|
font-size: 0.875rem;
|
|
flex-shrink: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.selected-guild-icon img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.selected-guild-name {
|
|
font-weight: 500;
|
|
font-size: 0.9rem;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.guild-selector-arrow {
|
|
color: var(--muted);
|
|
transition: transform 0.2s;
|
|
}
|
|
|
|
.guild-selector-header.open .guild-selector-arrow {
|
|
transform: rotate(180deg);
|
|
}
|
|
|
|
.guild-dropdown {
|
|
display: none;
|
|
background: var(--card);
|
|
border: 1px solid var(--card-border);
|
|
border-top: none;
|
|
border-radius: 0 0 12px 12px;
|
|
max-height: 280px;
|
|
overflow-y: auto;
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
|
|
.guild-dropdown.open {
|
|
display: block;
|
|
}
|
|
|
|
.guild-card {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
padding: 0.875rem 1rem;
|
|
cursor: pointer;
|
|
transition: all 0.15s;
|
|
border-left: 3px solid transparent;
|
|
}
|
|
|
|
.guild-card:hover {
|
|
background: rgba(99, 102, 241, 0.08);
|
|
}
|
|
|
|
.guild-card.active {
|
|
background: rgba(99, 102, 241, 0.12);
|
|
border-left-color: var(--primary);
|
|
}
|
|
|
|
.guild-card:last-child {
|
|
border-radius: 0 0 10px 10px;
|
|
}
|
|
|
|
.guild-icon {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 10px;
|
|
background: linear-gradient(135deg, var(--gradient-1), var(--gradient-2));
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: 600;
|
|
font-size: 0.9rem;
|
|
flex-shrink: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.guild-icon img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.guild-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.guild-name {
|
|
font-weight: 500;
|
|
font-size: 0.9rem;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.guild-status {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
font-size: 0.75rem;
|
|
color: var(--muted);
|
|
margin-top: 0.125rem;
|
|
}
|
|
|
|
.guild-admin-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
padding: 0.125rem 0.5rem;
|
|
background: rgba(99, 102, 241, 0.15);
|
|
border: 1px solid rgba(99, 102, 241, 0.3);
|
|
border-radius: 6px;
|
|
color: var(--primary-light);
|
|
font-size: 0.65rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.03em;
|
|
}
|
|
|
|
.guild-member-count {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
.guild-check {
|
|
color: var(--primary);
|
|
opacity: 0;
|
|
transition: opacity 0.15s;
|
|
}
|
|
|
|
.guild-card.active .guild-check {
|
|
opacity: 1;
|
|
}
|
|
|
|
.nav-section {
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.nav-section-title {
|
|
font-size: 0.7rem;
|
|
text-transform: uppercase;
|
|
color: var(--muted);
|
|
margin-bottom: 0.75rem;
|
|
font-weight: 600;
|
|
letter-spacing: 0.08em;
|
|
padding-left: 0.5rem;
|
|
}
|
|
|
|
.nav-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
padding: 0.75rem 1rem;
|
|
border-radius: 10px;
|
|
color: var(--muted-foreground);
|
|
text-decoration: none;
|
|
transition: all 0.2s;
|
|
cursor: pointer;
|
|
margin-bottom: 0.25rem;
|
|
font-size: 0.9rem;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.nav-item:hover {
|
|
background: rgba(99, 102, 241, 0.08);
|
|
color: var(--foreground);
|
|
}
|
|
|
|
.nav-item.active {
|
|
background: linear-gradient(135deg, rgba(99, 102, 241, 0.15), rgba(59, 130, 246, 0.1));
|
|
border: 1px solid rgba(99, 102, 241, 0.25);
|
|
color: var(--foreground);
|
|
}
|
|
|
|
.nav-icon { width: 20px; text-align: center; font-size: 1rem; }
|
|
|
|
.user-card {
|
|
margin-top: auto;
|
|
padding: 1rem;
|
|
background: var(--card);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 14px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
|
|
.user-avatar {
|
|
width: 44px;
|
|
height: 44px;
|
|
border-radius: 50%;
|
|
background: linear-gradient(135deg, var(--gradient-1), var(--gradient-2));
|
|
object-fit: cover;
|
|
}
|
|
|
|
.user-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.user-name {
|
|
font-weight: 600;
|
|
font-size: 0.9rem;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.user-level {
|
|
font-size: 0.75rem;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.main {
|
|
flex: 1;
|
|
margin-left: 280px;
|
|
padding: 2.5rem;
|
|
max-width: calc(100vw - 280px);
|
|
}
|
|
|
|
.page-header {
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.page-title {
|
|
font-size: 2rem;
|
|
font-weight: 700;
|
|
margin-bottom: 0.5rem;
|
|
letter-spacing: -0.02em;
|
|
}
|
|
|
|
.page-subtitle {
|
|
color: var(--muted);
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.text-gradient {
|
|
background: linear-gradient(135deg, var(--gradient-1), var(--gradient-2), var(--gradient-3));
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
background-size: 200% 200%;
|
|
animation: gradient-shift 4s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes gradient-shift {
|
|
0%, 100% { background-position: 0% 50%; }
|
|
50% { background-position: 100% 50%; }
|
|
}
|
|
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
gap: 1rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.stat-card {
|
|
background: var(--card);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 16px;
|
|
padding: 1.5rem;
|
|
backdrop-filter: blur(10px);
|
|
transition: all 0.3s;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.stat-card::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 1px;
|
|
background: linear-gradient(90deg, transparent, var(--primary), transparent);
|
|
opacity: 0;
|
|
transition: opacity 0.3s;
|
|
}
|
|
|
|
.stat-card:hover {
|
|
border-color: var(--card-border-hover);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.stat-card:hover::before {
|
|
opacity: 1;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 0.8rem;
|
|
color: var(--muted);
|
|
margin-bottom: 0.5rem;
|
|
font-weight: 500;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 1.75rem;
|
|
font-weight: 700;
|
|
background: linear-gradient(135deg, var(--gradient-1), var(--gradient-2));
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.stat-sub {
|
|
font-size: 0.75rem;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.card {
|
|
background: var(--card);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 16px;
|
|
margin-bottom: 1.5rem;
|
|
backdrop-filter: blur(10px);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.card-header {
|
|
padding: 1.25rem 1.5rem;
|
|
border-bottom: 1px solid var(--border);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.card-title {
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.card-body { padding: 1.5rem; }
|
|
|
|
.progress-bar {
|
|
height: 8px;
|
|
background: rgba(51, 65, 85, 0.5);
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
margin-top: 0.5rem;
|
|
}
|
|
|
|
.progress-fill {
|
|
height: 100%;
|
|
background: linear-gradient(90deg, var(--gradient-1), var(--gradient-2));
|
|
border-radius: 4px;
|
|
transition: width 0.5s ease;
|
|
}
|
|
|
|
.achievement-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
gap: 1rem;
|
|
}
|
|
|
|
.achievement-item {
|
|
display: flex;
|
|
gap: 1rem;
|
|
padding: 1.25rem;
|
|
background: var(--card);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 14px;
|
|
transition: all 0.3s;
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
|
|
.achievement-item:hover {
|
|
border-color: var(--card-border-hover);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.achievement-item.locked {
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.achievement-icon {
|
|
width: 52px;
|
|
height: 52px;
|
|
background: linear-gradient(135deg, rgba(99, 102, 241, 0.2), rgba(59, 130, 246, 0.1));
|
|
border: 1px solid rgba(99, 102, 241, 0.3);
|
|
border-radius: 12px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 1.5rem;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.achievement-info h4 {
|
|
font-weight: 600;
|
|
margin-bottom: 0.25rem;
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
.achievement-info p {
|
|
font-size: 0.85rem;
|
|
color: var(--muted);
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.quest-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 1.25rem;
|
|
background: var(--card);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 14px;
|
|
margin-bottom: 0.75rem;
|
|
transition: all 0.3s;
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
|
|
.quest-item:hover {
|
|
border-color: var(--card-border-hover);
|
|
}
|
|
|
|
.quest-info { flex: 1; }
|
|
|
|
.quest-name {
|
|
font-weight: 600;
|
|
margin-bottom: 0.25rem;
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
.quest-desc {
|
|
font-size: 0.85rem;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.quest-progress {
|
|
text-align: right;
|
|
min-width: 100px;
|
|
}
|
|
|
|
.quest-progress-text {
|
|
font-weight: 600;
|
|
background: linear-gradient(135deg, var(--gradient-1), var(--gradient-2));
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
}
|
|
|
|
.shop-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
|
gap: 1rem;
|
|
}
|
|
|
|
.shop-item {
|
|
background: var(--card);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 16px;
|
|
padding: 1.5rem;
|
|
text-align: center;
|
|
transition: all 0.3s;
|
|
backdrop-filter: blur(10px);
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.shop-item::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 1px;
|
|
background: linear-gradient(90deg, transparent, var(--primary), transparent);
|
|
opacity: 0;
|
|
transition: opacity 0.3s;
|
|
}
|
|
|
|
.shop-item:hover {
|
|
border-color: var(--card-border-hover);
|
|
transform: translateY(-4px);
|
|
}
|
|
|
|
.shop-item:hover::before {
|
|
opacity: 1;
|
|
}
|
|
|
|
.shop-icon {
|
|
font-size: 2.5rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.shop-name {
|
|
font-weight: 600;
|
|
margin-bottom: 0.5rem;
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
.shop-price {
|
|
background: linear-gradient(135deg, var(--gradient-1), var(--gradient-2));
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.625rem 1.25rem;
|
|
border-radius: 10px;
|
|
font-weight: 600;
|
|
text-decoration: none;
|
|
transition: all 0.2s;
|
|
border: none;
|
|
cursor: pointer;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: linear-gradient(135deg, var(--gradient-1), var(--gradient-2));
|
|
color: white;
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 8px 24px rgba(99, 102, 241, 0.4);
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: var(--secondary);
|
|
border: 1px solid var(--border);
|
|
color: var(--foreground);
|
|
}
|
|
|
|
.btn-secondary:hover {
|
|
background: rgba(51, 65, 85, 0.5);
|
|
border-color: var(--primary);
|
|
}
|
|
|
|
.btn-icon {
|
|
padding: 0.5rem;
|
|
background: transparent;
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
color: var(--muted);
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.btn-icon:hover {
|
|
background: rgba(99, 102, 241, 0.1);
|
|
border-color: var(--primary);
|
|
color: var(--foreground);
|
|
}
|
|
|
|
.btn-icon.danger:hover {
|
|
background: rgba(239, 68, 68, 0.1);
|
|
border-color: var(--danger);
|
|
color: var(--danger);
|
|
}
|
|
|
|
.leaderboard-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
padding: 1rem 1.25rem;
|
|
border-radius: 12px;
|
|
margin-bottom: 0.5rem;
|
|
background: var(--card);
|
|
border: 1px solid var(--card-border);
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.leaderboard-item:hover {
|
|
border-color: var(--card-border-hover);
|
|
}
|
|
|
|
.leaderboard-item.top-3 {
|
|
background: linear-gradient(135deg, rgba(99, 102, 241, 0.12), rgba(59, 130, 246, 0.08));
|
|
border: 1px solid rgba(99, 102, 241, 0.3);
|
|
}
|
|
|
|
.leaderboard-rank {
|
|
width: 36px;
|
|
height: 36px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: 700;
|
|
border-radius: 10px;
|
|
background: rgba(51, 65, 85, 0.5);
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.leaderboard-item.top-3 .leaderboard-rank {
|
|
background: linear-gradient(135deg, var(--gradient-1), var(--gradient-2));
|
|
}
|
|
|
|
.leaderboard-avatar {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 50%;
|
|
background: linear-gradient(135deg, var(--gradient-1), var(--gradient-2));
|
|
object-fit: cover;
|
|
}
|
|
|
|
.leaderboard-name { flex: 1; font-weight: 500; }
|
|
|
|
.leaderboard-xp {
|
|
background: linear-gradient(135deg, var(--gradient-1), var(--gradient-2));
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.tabs {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
margin-bottom: 1.5rem;
|
|
padding-bottom: 1rem;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.tab {
|
|
padding: 0.625rem 1.25rem;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
color: var(--muted);
|
|
transition: all 0.2s;
|
|
font-weight: 500;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.tab:hover {
|
|
color: var(--foreground);
|
|
background: rgba(99, 102, 241, 0.08);
|
|
}
|
|
|
|
.tab.active {
|
|
background: linear-gradient(135deg, var(--gradient-1), var(--gradient-2));
|
|
color: white;
|
|
}
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 4rem 2rem;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.empty-state-icon {
|
|
font-size: 3.5rem;
|
|
margin-bottom: 1rem;
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.login-prompt {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-height: 100vh;
|
|
text-align: center;
|
|
padding: 2rem;
|
|
}
|
|
|
|
.login-prompt h1 {
|
|
font-size: 2.5rem;
|
|
font-weight: 700;
|
|
margin-bottom: 1rem;
|
|
letter-spacing: -0.02em;
|
|
}
|
|
|
|
.login-prompt p {
|
|
color: var(--muted);
|
|
margin-bottom: 2.5rem;
|
|
font-size: 1.1rem;
|
|
}
|
|
|
|
.hidden { display: none !important; }
|
|
|
|
.loading-spinner {
|
|
display: inline-block;
|
|
width: 28px;
|
|
height: 28px;
|
|
border: 3px solid rgba(51, 65, 85, 0.5);
|
|
border-top-color: var(--primary);
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
|
|
.admin-form { margin-bottom: 2rem; }
|
|
|
|
.form-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
gap: 1.5rem;
|
|
}
|
|
|
|
.form-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.form-group label {
|
|
font-size: 0.85rem;
|
|
font-weight: 500;
|
|
color: var(--foreground);
|
|
}
|
|
|
|
.form-input {
|
|
padding: 0.75rem 1rem;
|
|
background: rgba(30, 41, 59, 0.5);
|
|
border: 1px solid var(--border);
|
|
border-radius: 10px;
|
|
color: var(--foreground);
|
|
font-family: inherit;
|
|
font-size: 0.875rem;
|
|
transition: border-color 0.2s, box-shadow 0.2s;
|
|
}
|
|
|
|
.form-input:focus {
|
|
outline: none;
|
|
border-color: var(--primary);
|
|
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15);
|
|
}
|
|
|
|
.form-input::placeholder {
|
|
color: var(--muted);
|
|
}
|
|
|
|
.form-hint {
|
|
font-size: 0.75rem;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.form-toggle {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
}
|
|
|
|
.form-toggle input {
|
|
display: none;
|
|
}
|
|
|
|
.toggle-slider {
|
|
width: 48px;
|
|
height: 26px;
|
|
background: rgba(51, 65, 85, 0.5);
|
|
border: 1px solid var(--border);
|
|
border-radius: 13px;
|
|
position: relative;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.toggle-slider::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 2px;
|
|
left: 2px;
|
|
width: 20px;
|
|
height: 20px;
|
|
background: var(--muted);
|
|
border-radius: 50%;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.form-toggle input:checked + .toggle-slider {
|
|
background: var(--primary);
|
|
border-color: var(--primary);
|
|
}
|
|
|
|
.form-toggle input:checked + .toggle-slider::before {
|
|
left: 24px;
|
|
background: white;
|
|
}
|
|
|
|
.toggle-label {
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.form-actions {
|
|
display: flex;
|
|
gap: 1rem;
|
|
margin-top: 1.5rem;
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
.save-status {
|
|
margin-top: 1rem;
|
|
padding: 0.875rem 1.25rem;
|
|
border-radius: 10px;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.save-status.success {
|
|
background: rgba(16, 185, 129, 0.1);
|
|
border: 1px solid var(--success);
|
|
color: var(--success);
|
|
}
|
|
|
|
.save-status.error {
|
|
background: rgba(239, 68, 68, 0.1);
|
|
border: 1px solid var(--danger);
|
|
color: var(--danger);
|
|
}
|
|
|
|
textarea.form-input {
|
|
min-height: 100px;
|
|
resize: vertical;
|
|
}
|
|
|
|
.item-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.item-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
padding: 1.25rem;
|
|
background: var(--card);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 14px;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.item-row:hover {
|
|
border-color: var(--card-border-hover);
|
|
}
|
|
|
|
.item-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.item-name {
|
|
font-weight: 600;
|
|
margin-bottom: 0.25rem;
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
.item-desc {
|
|
font-size: 0.85rem;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.item-actions {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.item-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
padding: 0.25rem 0.625rem;
|
|
border-radius: 6px;
|
|
font-size: 0.7rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.03em;
|
|
margin-left: 0.5rem;
|
|
}
|
|
|
|
.item-badge.active {
|
|
background: rgba(16, 185, 129, 0.15);
|
|
color: var(--success);
|
|
border: 1px solid rgba(16, 185, 129, 0.3);
|
|
}
|
|
|
|
.item-badge.inactive {
|
|
background: rgba(245, 158, 11, 0.15);
|
|
color: var(--warning);
|
|
border: 1px solid rgba(245, 158, 11, 0.3);
|
|
}
|
|
|
|
.item-badge.hidden {
|
|
background: rgba(100, 116, 139, 0.15);
|
|
color: var(--muted);
|
|
border: 1px solid rgba(100, 116, 139, 0.3);
|
|
}
|
|
|
|
.modal {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(3, 7, 18, 0.9);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 1000;
|
|
padding: 1rem;
|
|
backdrop-filter: blur(8px);
|
|
}
|
|
|
|
.modal-content {
|
|
background: rgba(15, 23, 42, 0.95);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 20px;
|
|
max-width: 600px;
|
|
width: 100%;
|
|
max-height: 90vh;
|
|
overflow-y: auto;
|
|
backdrop-filter: blur(20px);
|
|
}
|
|
|
|
.modal-header {
|
|
padding: 1.5rem 1.75rem;
|
|
border-bottom: 1px solid var(--border);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.modal-header h3 {
|
|
font-size: 1.125rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.modal-body {
|
|
padding: 1.75rem;
|
|
}
|
|
|
|
.modal-footer {
|
|
padding: 1.25rem 1.75rem;
|
|
border-top: 1px solid var(--border);
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.mobile-menu-btn {
|
|
display: none;
|
|
position: fixed;
|
|
top: 1rem;
|
|
left: 1rem;
|
|
z-index: 200;
|
|
width: 44px;
|
|
height: 44px;
|
|
background: var(--card);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 10px;
|
|
cursor: pointer;
|
|
align-items: center;
|
|
justify-content: center;
|
|
backdrop-filter: blur(10px);
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.mobile-menu-btn:hover {
|
|
border-color: var(--primary);
|
|
background: rgba(99, 102, 241, 0.1);
|
|
}
|
|
|
|
.mobile-menu-btn svg {
|
|
color: var(--foreground);
|
|
}
|
|
|
|
.sidebar-overlay {
|
|
display: none;
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.6);
|
|
z-index: 90;
|
|
backdrop-filter: blur(4px);
|
|
}
|
|
|
|
.sidebar-overlay.open {
|
|
display: block;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.mobile-menu-btn {
|
|
display: flex;
|
|
}
|
|
.sidebar {
|
|
transform: translateX(-100%);
|
|
z-index: 100;
|
|
transition: transform 0.3s ease;
|
|
}
|
|
.sidebar.open {
|
|
transform: translateX(0);
|
|
box-shadow: 4px 0 24px rgba(0, 0, 0, 0.5);
|
|
}
|
|
.main {
|
|
margin-left: 0;
|
|
max-width: 100%;
|
|
padding: 1rem;
|
|
padding-top: 4.5rem;
|
|
}
|
|
.stats-grid { grid-template-columns: repeat(2, 1fr); }
|
|
.page-title { font-size: 1.5rem; }
|
|
.page-header { margin-bottom: 1.5rem; }
|
|
.form-grid { grid-template-columns: 1fr; }
|
|
.achievement-grid { grid-template-columns: 1fr; }
|
|
.shop-grid { grid-template-columns: repeat(2, 1fr); }
|
|
.tabs { flex-wrap: wrap; }
|
|
.modal-content { margin: 1rem; max-height: calc(100vh - 2rem); }
|
|
}
|
|
|
|
@media (max-width: 480px) {
|
|
.stats-grid { grid-template-columns: 1fr; }
|
|
.shop-grid { grid-template-columns: 1fr; }
|
|
.main { padding: 0.75rem; padding-top: 4.5rem; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="bg-grid"></div>
|
|
<div class="bg-glow"></div>
|
|
|
|
<div id="loginPrompt" class="login-prompt">
|
|
<h1>Welcome to <span class="text-gradient">AeThex</span></h1>
|
|
<p>Login with Discord to view your profile, achievements, and more.</p>
|
|
<a href="/auth/discord" class="btn btn-primary">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z"/></svg>
|
|
Login with Discord
|
|
</a>
|
|
</div>
|
|
|
|
<div id="app" class="app hidden">
|
|
<button class="mobile-menu-btn" id="mobileMenuBtn" onclick="toggleMobileSidebar()">
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<line x1="3" y1="6" x2="21" y2="6"></line>
|
|
<line x1="3" y1="12" x2="21" y2="12"></line>
|
|
<line x1="3" y1="18" x2="21" y2="18"></line>
|
|
</svg>
|
|
</button>
|
|
<div class="sidebar-overlay" id="sidebarOverlay" onclick="closeMobileSidebar()"></div>
|
|
<aside class="sidebar" id="sidebar">
|
|
<a href="/" class="logo">
|
|
<img src="/logo.png" alt="AeThex" class="logo-icon">
|
|
<span class="logo-text">AeThex | Warden</span>
|
|
</a>
|
|
|
|
<div class="guild-selector">
|
|
<div class="guild-selector-header" id="guildSelectorHeader" onclick="toggleGuildDropdown()">
|
|
<div class="selected-guild">
|
|
<div class="selected-guild-icon" id="selectedGuildIcon">?</div>
|
|
<span class="selected-guild-name" id="selectedGuildName">Select a server...</span>
|
|
</div>
|
|
<svg class="guild-selector-arrow" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M6 9l6 6 6-6"/>
|
|
</svg>
|
|
</div>
|
|
<div class="guild-dropdown" id="guildDropdown"></div>
|
|
</div>
|
|
|
|
<nav>
|
|
<div class="nav-section">
|
|
<div class="nav-section-title">Overview</div>
|
|
<div class="nav-item active" data-page="profile">
|
|
<span class="nav-icon">👤</span> Profile
|
|
</div>
|
|
<div class="nav-item" data-page="leaderboard">
|
|
<span class="nav-icon">🏆</span> Leaderboard
|
|
</div>
|
|
</div>
|
|
|
|
<div class="nav-section">
|
|
<div class="nav-section-title">Rewards</div>
|
|
<div class="nav-item" data-page="achievements">
|
|
<span class="nav-icon">🎖️</span> Achievements
|
|
</div>
|
|
<div class="nav-item" data-page="quests">
|
|
<span class="nav-icon">🎯</span> Quests
|
|
</div>
|
|
<div class="nav-item" data-page="shop">
|
|
<span class="nav-icon">🛒</span> Shop
|
|
</div>
|
|
<div class="nav-item" data-page="inventory">
|
|
<span class="nav-icon">🎒</span> Inventory
|
|
</div>
|
|
<div class="nav-item" data-page="titles">
|
|
<span class="nav-icon">🏷️</span> Titles
|
|
</div>
|
|
<div class="nav-item" data-page="coins">
|
|
<span class="nav-icon">🪙</span> Coins
|
|
</div>
|
|
</div>
|
|
|
|
<div class="nav-section" id="adminSection" style="display:none">
|
|
<div class="nav-section-title">Admin</div>
|
|
<div class="nav-item" data-page="admin-xp">
|
|
<span class="nav-icon">⚙️</span> XP Settings
|
|
</div>
|
|
<div class="nav-item" data-page="admin-quests">
|
|
<span class="nav-icon">📝</span> Manage Quests
|
|
</div>
|
|
<div class="nav-item" data-page="admin-achievements">
|
|
<span class="nav-icon">🏅</span> Manage Achievements
|
|
</div>
|
|
<div class="nav-item" data-page="admin-shop">
|
|
<span class="nav-icon">🏪</span> Manage Shop
|
|
</div>
|
|
<div class="nav-item" data-page="moderation">
|
|
<span class="nav-icon">🛡</span> Moderation
|
|
</div>
|
|
<div class="nav-item" data-page="analytics">
|
|
<span class="nav-icon">📊</span> Analytics
|
|
</div>
|
|
<div class="nav-item" data-page="activity-roles">
|
|
<span class="nav-icon">🎯</span> Activity Roles
|
|
</div>
|
|
<div class="nav-item" data-page="cooldowns">
|
|
<span class="nav-icon">⏱️</span> Cooldowns
|
|
</div>
|
|
<div class="nav-item" data-page="federation">
|
|
<span class="nav-icon">🌐</span> Federation
|
|
</div>
|
|
<div class="nav-item" data-page="backups">
|
|
<span class="nav-icon">💾</span> Backups
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<div class="user-card" id="userCard">
|
|
<img class="user-avatar" id="userAvatar" src="" alt="">
|
|
<div class="user-info">
|
|
<div class="user-name" id="userName">Loading...</div>
|
|
<div class="user-level" id="userLevel">Level 0</div>
|
|
</div>
|
|
<a href="/auth/logout" class="btn btn-secondary" style="padding:0.5rem">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9"/></svg>
|
|
</a>
|
|
</div>
|
|
</aside>
|
|
|
|
<main class="main">
|
|
<div id="page-profile" class="page">
|
|
<div class="page-header">
|
|
<h1 class="page-title">Your <span class="text-gradient">Profile</span></h1>
|
|
<p class="page-subtitle">Track your progress and stats</p>
|
|
</div>
|
|
|
|
<div class="stats-grid" id="statsGrid">
|
|
<div class="stat-card">
|
|
<div class="stat-label">Total XP</div>
|
|
<div class="stat-value" id="statXp">0</div>
|
|
<div class="stat-sub" id="statXpProgress">0 XP to next level</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Level</div>
|
|
<div class="stat-value" id="statLevel">0</div>
|
|
<div class="stat-sub" id="statPrestige">Prestige 0</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Messages</div>
|
|
<div class="stat-value" id="statMessages">0</div>
|
|
<div class="stat-sub">All time</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Voice Time</div>
|
|
<div class="stat-value" id="statVoice">0h</div>
|
|
<div class="stat-sub">All time</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Daily Streak</div>
|
|
<div class="stat-value" id="statStreak">0</div>
|
|
<div class="stat-sub">days</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Achievements</div>
|
|
<div class="stat-value" id="statAchievements">0</div>
|
|
<div class="stat-sub">earned</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label" id="statCoinLabel">Coins</div>
|
|
<div class="stat-value" id="statCoins">0</div>
|
|
<div class="stat-sub">server currency</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Level Progress</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<div style="display:flex;justify-content:space-between;margin-bottom:0.5rem">
|
|
<span>Level <span id="currentLevel">0</span></span>
|
|
<span>Level <span id="nextLevel">1</span></span>
|
|
</div>
|
|
<div class="progress-bar">
|
|
<div class="progress-fill" id="levelProgress" style="width:0%"></div>
|
|
</div>
|
|
<p style="margin-top:0.75rem;color:var(--muted);font-size:0.875rem">
|
|
<span id="xpCurrent">0</span> / <span id="xpRequired">100</span> XP
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="page-leaderboard" class="page hidden">
|
|
<div class="page-header">
|
|
<h1 class="page-title"><span class="text-gradient">Leaderboard</span></h1>
|
|
<p class="page-subtitle">See who's on top</p>
|
|
</div>
|
|
|
|
<div class="tabs">
|
|
<div class="tab active" data-lb-type="all">All Time</div>
|
|
<div class="tab" data-lb-type="monthly">Monthly</div>
|
|
<div class="tab" data-lb-type="weekly">Weekly</div>
|
|
</div>
|
|
|
|
<div id="leaderboardList">
|
|
<div class="empty-state">
|
|
<div class="empty-state-icon">🏆</div>
|
|
<p>Select a server to view the leaderboard</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="page-achievements" class="page hidden">
|
|
<div class="page-header">
|
|
<h1 class="page-title"><span class="text-gradient">Achievements</span></h1>
|
|
<p class="page-subtitle">Collect badges and show off your accomplishments</p>
|
|
</div>
|
|
|
|
<div class="achievement-grid" id="achievementGrid">
|
|
<div class="empty-state" style="grid-column:1/-1">
|
|
<div class="empty-state-icon">🎖️</div>
|
|
<p>Select a server to view achievements</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="page-quests" class="page hidden">
|
|
<div class="page-header">
|
|
<h1 class="page-title"><span class="text-gradient">Quests</span></h1>
|
|
<p class="page-subtitle">Complete challenges to earn rewards</p>
|
|
</div>
|
|
|
|
<div id="questList">
|
|
<div class="empty-state">
|
|
<div class="empty-state-icon">🎯</div>
|
|
<p>Select a server to view quests</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="page-shop" class="page hidden">
|
|
<div class="page-header">
|
|
<h1 class="page-title"><span class="text-gradient">Shop</span></h1>
|
|
<p class="page-subtitle">Spend your XP on rewards</p>
|
|
</div>
|
|
|
|
<div class="shop-grid" id="shopGrid">
|
|
<div class="empty-state" style="grid-column:1/-1">
|
|
<div class="empty-state-icon">🛒</div>
|
|
<p>Select a server to view shop items</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="page-inventory" class="page hidden">
|
|
<div class="page-header">
|
|
<h1 class="page-title"><span class="text-gradient">Inventory</span></h1>
|
|
<p class="page-subtitle">Your purchased items and perks</p>
|
|
</div>
|
|
|
|
<div class="shop-grid" id="inventoryGrid">
|
|
<div class="empty-state" style="grid-column:1/-1">
|
|
<div class="empty-state-icon">🎒</div>
|
|
<p>Your inventory is empty</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="page-titles" class="page hidden">
|
|
<div class="page-header">
|
|
<h1 class="page-title"><span class="text-gradient">Titles</span></h1>
|
|
<p class="page-subtitle">Customize your display title that shows on your profile</p>
|
|
</div>
|
|
|
|
<div class="card" style="margin-bottom:1.5rem">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Current Title</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<div id="currentTitleDisplay" style="display:flex;align-items:center;gap:1rem">
|
|
<div style="flex:1">
|
|
<div style="font-size:1.25rem;font-weight:600" id="activeTitleName">No title selected</div>
|
|
<div style="font-size:0.875rem;color:var(--muted)" id="activeTitleDesc">Select a title from your collection below</div>
|
|
</div>
|
|
<button class="btn btn-secondary" onclick="clearActiveTitle()" id="clearTitleBtn" style="display:none">Clear Title</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Your Titles</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<div id="titlesList" class="item-list">
|
|
<div class="empty-state">
|
|
<div class="empty-state-icon">🏷️</div>
|
|
<p>You don't have any titles yet</p>
|
|
<p style="font-size:0.875rem;color:var(--muted);margin-top:0.5rem">Purchase titles from the shop or earn them through achievements</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="page-coins" class="page hidden">
|
|
<div class="page-header">
|
|
<h1 class="page-title"><span class="text-gradient" id="coinPageTitle">Coins</span></h1>
|
|
<p class="page-subtitle">Server currency you can earn and spend</p>
|
|
</div>
|
|
|
|
<div class="stats-grid">
|
|
<div class="stat-card">
|
|
<div class="stat-label" id="coinBalanceLabel">Your Balance</div>
|
|
<div class="stat-value" id="coinBalance">0</div>
|
|
<div class="stat-sub" id="coinCurrencyName">Coins</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card" style="margin-top:1.5rem">
|
|
<div class="card-header">
|
|
<h3 class="card-title" id="coinLeaderboardTitle">Coin Leaderboard</h3>
|
|
</div>
|
|
<div class="card-body" id="coinLeaderboardBody">
|
|
<div class="empty-state">
|
|
<div class="empty-state-icon">🪙</div>
|
|
<p>No coin leaderboard data yet</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card" style="margin-top:1.5rem" id="coinAdminCard" style="display:none">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Coin Settings (Admin)</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<form id="coinSettingsForm" onsubmit="saveCoinConfig(event)">
|
|
<div class="form-grid">
|
|
<div class="form-group">
|
|
<label for="coinName">Currency Name</label>
|
|
<input type="text" id="coinName" name="coin_name" value="Coins" class="form-input" placeholder="e.g., Coins, Gold, Credits">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="messageCoins">Coins per Message</label>
|
|
<input type="number" id="messageCoins" name="message_coins" min="0" max="100" value="1" class="form-input">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="dailyCoins">Daily Claim Coins</label>
|
|
<input type="number" id="dailyCoins" name="daily_coins" min="0" max="1000" value="50" class="form-input">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="toggle-label">
|
|
<input type="checkbox" id="coinsEnabled" name="coins_enabled" checked>
|
|
<span>Coins Enabled</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div style="margin-top:1.5rem">
|
|
<button type="submit" class="btn btn-primary">Save Coin Settings</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="page-admin-xp" class="page hidden">
|
|
<div class="page-header">
|
|
<h1 class="page-title"><span class="text-gradient">XP Settings</span></h1>
|
|
<p class="page-subtitle">Configure XP earning for your server</p>
|
|
</div>
|
|
|
|
<form id="xpSettingsForm" class="admin-form" onsubmit="saveXpConfig(event)">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Message XP</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="form-grid">
|
|
<div class="form-group">
|
|
<label for="xpMsgMin">Min XP per Message</label>
|
|
<input type="number" id="xpMsgMin" name="message_xp_min" min="0" max="1000" value="15" class="form-input">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="xpMsgMax">Max XP per Message</label>
|
|
<input type="number" id="xpMsgMax" name="message_xp_max" min="0" max="1000" value="25" class="form-input">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="xpMsgCooldown">Cooldown (seconds)</label>
|
|
<input type="number" id="xpMsgCooldown" name="message_cooldown" min="0" max="3600" value="60" class="form-input">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Other XP Sources</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="form-grid">
|
|
<div class="form-group">
|
|
<label for="xpReaction">Reaction XP</label>
|
|
<input type="number" id="xpReaction" name="reaction_xp" min="0" max="100" value="5" class="form-input">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="xpVoice">Voice XP per Minute</label>
|
|
<input type="number" id="xpVoice" name="voice_xp_per_minute" min="0" max="100" value="2" class="form-input">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="xpDaily">Daily Claim XP</label>
|
|
<input type="number" id="xpDaily" name="daily_xp" min="0" max="10000" value="100" class="form-input">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Leveling Settings</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="form-grid">
|
|
<div class="form-group">
|
|
<label for="xpBase">XP Base (difficulty)</label>
|
|
<input type="number" id="xpBase" name="xp_base" min="10" max="1000" value="100" class="form-input">
|
|
<span class="form-hint">Lower = easier leveling</span>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="xpMultiplier">Global XP Multiplier</label>
|
|
<input type="number" id="xpMultiplier" name="xp_multiplier" min="0.1" max="10" step="0.1" value="1.0" class="form-input">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="weekendMultiplier">Weekend Multiplier</label>
|
|
<input type="number" id="weekendMultiplier" name="weekend_multiplier" min="1" max="5" step="0.1" value="1.5" class="form-input">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Level-Up Announcements</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="form-grid">
|
|
<div class="form-group">
|
|
<label for="levelUpChannel">Announcement Channel ID</label>
|
|
<input type="text" id="levelUpChannel" name="levelup_channel_id" placeholder="Leave blank for current channel" class="form-input">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-toggle">
|
|
<input type="checkbox" id="levelUpEnabled" name="levelup_enabled" checked>
|
|
<span class="toggle-slider"></span>
|
|
<span class="toggle-label">Enable Level-Up Messages</span>
|
|
</label>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-toggle">
|
|
<input type="checkbox" id="levelUpDm" name="levelup_dm">
|
|
<span class="toggle-slider"></span>
|
|
<span class="toggle-label">Send Level-Up via DM</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-actions">
|
|
<button type="button" class="btn btn-secondary" onclick="loadXpConfig()">Reset</button>
|
|
<button type="submit" class="btn btn-primary">Save Settings</button>
|
|
</div>
|
|
|
|
<div id="xpSaveStatus" class="save-status hidden"></div>
|
|
</form>
|
|
</div>
|
|
|
|
<div id="page-admin-quests" class="page hidden">
|
|
<div class="page-header" style="display:flex;justify-content:space-between;align-items:flex-start">
|
|
<div>
|
|
<h1 class="page-title"><span class="text-gradient">Manage Quests</span></h1>
|
|
<p class="page-subtitle">Create and edit server quests</p>
|
|
</div>
|
|
<button class="btn btn-primary" onclick="openQuestModal()">+ Create Quest</button>
|
|
</div>
|
|
|
|
<div id="adminQuestList" class="item-list">
|
|
<div class="empty-state">
|
|
<div class="empty-state-icon">🎯</div>
|
|
<p>No quests configured yet</p>
|
|
</div>
|
|
</div>
|
|
<div id="questSaveStatus" class="save-status hidden"></div>
|
|
</div>
|
|
|
|
<div id="questModal" class="modal hidden">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h3 id="questModalTitle">Create Quest</h3>
|
|
<button class="btn-icon" onclick="closeQuestModal()">×</button>
|
|
</div>
|
|
<form id="questForm" onsubmit="saveQuest(event)">
|
|
<div class="modal-body">
|
|
<div class="form-grid" style="grid-template-columns:1fr">
|
|
<div class="form-group">
|
|
<label for="questName">Quest Name</label>
|
|
<input type="text" id="questName" class="form-input" required placeholder="e.g., Message Master">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="questDescription">Description</label>
|
|
<textarea id="questDescription" class="form-input" rows="2" placeholder="e.g., Send 50 messages in the server"></textarea>
|
|
</div>
|
|
<div class="form-grid">
|
|
<div class="form-group">
|
|
<label for="questType">Quest Type</label>
|
|
<select id="questType" class="form-input" required>
|
|
<option value="daily">Daily</option>
|
|
<option value="weekly">Weekly</option>
|
|
<option value="special">Special</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="questObjective">Objective</label>
|
|
<select id="questObjective" class="form-input" required>
|
|
<option value="messages">Send Messages</option>
|
|
<option value="reactions">Add Reactions</option>
|
|
<option value="voice_minutes">Voice Chat (minutes)</option>
|
|
<option value="commands">Use Commands</option>
|
|
<option value="daily_claims">Claim Daily Rewards</option>
|
|
<option value="level_ups">Level Up</option>
|
|
<option value="xp_earned">Earn XP</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="form-grid">
|
|
<div class="form-group">
|
|
<label for="questTarget">Target Amount</label>
|
|
<input type="number" id="questTarget" class="form-input" min="1" value="10" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="questXpReward">XP Reward</label>
|
|
<input type="number" id="questXpReward" class="form-input" min="1" value="100" required>
|
|
</div>
|
|
</div>
|
|
<div class="form-grid">
|
|
<div class="form-group">
|
|
<label for="questDuration">Duration (hours)</label>
|
|
<input type="number" id="questDuration" class="form-input" min="0" value="24" placeholder="0 = no expiry">
|
|
<span class="form-hint">Leave 0 for no expiration</span>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-toggle" style="margin-top:1.5rem">
|
|
<input type="checkbox" id="questRepeatable">
|
|
<span class="toggle-slider"></span>
|
|
<span class="toggle-label">Repeatable</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-toggle">
|
|
<input type="checkbox" id="questActive" checked>
|
|
<span class="toggle-slider"></span>
|
|
<span class="toggle-label">Active</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" onclick="closeQuestModal()">Cancel</button>
|
|
<button type="submit" class="btn btn-primary">Save Quest</button>
|
|
</div>
|
|
<input type="hidden" id="questEditId" value="">
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="page-admin-achievements" class="page hidden">
|
|
<div class="page-header" style="display:flex;justify-content:space-between;align-items:flex-start">
|
|
<div>
|
|
<h1 class="page-title"><span class="text-gradient">Manage Achievements</span></h1>
|
|
<p class="page-subtitle">Create custom server achievements</p>
|
|
</div>
|
|
<button class="btn btn-primary" onclick="openAchievementModal()">+ Create Achievement</button>
|
|
</div>
|
|
|
|
<div id="adminAchievementList" class="item-list">
|
|
<div class="empty-state">
|
|
<div class="empty-state-icon">🏅</div>
|
|
<p>No achievements configured yet</p>
|
|
</div>
|
|
</div>
|
|
<div id="achievementSaveStatus" class="save-status hidden"></div>
|
|
</div>
|
|
|
|
<div id="achievementModal" class="modal hidden">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h3 id="achievementModalTitle">Create Achievement</h3>
|
|
<button class="btn-icon" onclick="closeAchievementModal()">×</button>
|
|
</div>
|
|
<form id="achievementForm" onsubmit="saveAchievement(event)">
|
|
<div class="modal-body">
|
|
<div class="form-grid" style="grid-template-columns:1fr">
|
|
<div class="form-grid">
|
|
<div class="form-group">
|
|
<label for="achievementName">Achievement Name</label>
|
|
<input type="text" id="achievementName" class="form-input" required placeholder="e.g., Message Champion">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="achievementIcon">Icon (emoji)</label>
|
|
<input type="text" id="achievementIcon" class="form-input" placeholder="e.g., 🏆" value="🏆">
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="achievementDescription">Description</label>
|
|
<textarea id="achievementDescription" class="form-input" rows="2" placeholder="e.g., Send 1000 messages in the server"></textarea>
|
|
</div>
|
|
<div class="form-grid">
|
|
<div class="form-group">
|
|
<label for="achievementTrigger">Trigger Type</label>
|
|
<select id="achievementTrigger" class="form-input" required>
|
|
<option value="level">Reach Level</option>
|
|
<option value="prestige">Reach Prestige</option>
|
|
<option value="total_xp">Total XP Earned</option>
|
|
<option value="messages">Messages Sent</option>
|
|
<option value="reactions_given">Reactions Given</option>
|
|
<option value="reactions_received">Reactions Received</option>
|
|
<option value="voice_minutes">Voice Minutes</option>
|
|
<option value="daily_streak">Daily Streak</option>
|
|
<option value="commands_used">Commands Used</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="achievementValue">Trigger Value</label>
|
|
<input type="number" id="achievementValue" class="form-input" min="1" value="10" required>
|
|
<span class="form-hint">e.g., Level 10, 1000 XP, 50 messages</span>
|
|
</div>
|
|
</div>
|
|
<div class="form-grid">
|
|
<div class="form-group">
|
|
<label for="achievementXpReward">XP Reward</label>
|
|
<input type="number" id="achievementXpReward" class="form-input" min="0" value="100">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="achievementRoleReward">Role Reward ID</label>
|
|
<input type="text" id="achievementRoleReward" class="form-input" placeholder="Leave empty for none">
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-toggle">
|
|
<input type="checkbox" id="achievementHidden">
|
|
<span class="toggle-slider"></span>
|
|
<span class="toggle-label">Hidden Achievement</span>
|
|
</label>
|
|
<span class="form-hint">Hidden achievements are not shown until earned</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" onclick="closeAchievementModal()">Cancel</button>
|
|
<button type="submit" class="btn btn-primary">Save Achievement</button>
|
|
</div>
|
|
<input type="hidden" id="achievementEditId" value="">
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="page-admin-shop" class="page hidden">
|
|
<div class="page-header" style="display:flex;justify-content:space-between;align-items:flex-start">
|
|
<div>
|
|
<h1 class="page-title"><span class="text-gradient">Manage Shop</span></h1>
|
|
<p class="page-subtitle">Configure shop items and prices</p>
|
|
</div>
|
|
<button class="btn btn-primary" onclick="openShopModal()">+ Add Item</button>
|
|
</div>
|
|
|
|
<div id="adminShopList" class="item-list">
|
|
<div class="empty-state">
|
|
<div class="empty-state-icon">🏪</div>
|
|
<p>No shop items configured yet</p>
|
|
</div>
|
|
</div>
|
|
<div id="shopSaveStatus" class="save-status hidden"></div>
|
|
</div>
|
|
|
|
<div id="shopModal" class="modal hidden">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h3 id="shopModalTitle">Add Shop Item</h3>
|
|
<button class="btn-icon" onclick="closeShopModal()">×</button>
|
|
</div>
|
|
<form id="shopForm" onsubmit="saveShopItem(event)">
|
|
<div class="modal-body">
|
|
<div class="form-grid" style="grid-template-columns:1fr">
|
|
<div class="form-grid">
|
|
<div class="form-group">
|
|
<label for="shopItemName">Item Name</label>
|
|
<input type="text" id="shopItemName" class="form-input" required placeholder="e.g., Gold Badge">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="shopItemType">Item Type</label>
|
|
<select id="shopItemType" class="form-input" required>
|
|
<option value="badge">Badge</option>
|
|
<option value="title">Title</option>
|
|
<option value="background">Background</option>
|
|
<option value="booster">XP Booster</option>
|
|
<option value="role">Role</option>
|
|
<option value="special">Special</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="shopItemDescription">Description</label>
|
|
<textarea id="shopItemDescription" class="form-input" rows="2" placeholder="Describe what this item does"></textarea>
|
|
</div>
|
|
<div class="form-grid">
|
|
<div class="form-group">
|
|
<label for="shopItemPrice">Price (XP)</label>
|
|
<input type="number" id="shopItemPrice" class="form-input" min="1" value="100" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="shopItemStock">Stock</label>
|
|
<input type="number" id="shopItemStock" class="form-input" min="-1" placeholder="-1 = unlimited">
|
|
<span class="form-hint">Leave empty or -1 for unlimited</span>
|
|
</div>
|
|
</div>
|
|
<div class="form-grid">
|
|
<div class="form-group">
|
|
<label for="shopItemLevelReq">Level Required</label>
|
|
<input type="number" id="shopItemLevelReq" class="form-input" min="0" value="0">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="shopItemPrestigeReq">Prestige Required</label>
|
|
<input type="number" id="shopItemPrestigeReq" class="form-input" min="0" value="0">
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-toggle">
|
|
<input type="checkbox" id="shopItemEnabled" checked>
|
|
<span class="toggle-slider"></span>
|
|
<span class="toggle-label">Available in Shop</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" onclick="closeShopModal()">Cancel</button>
|
|
<button type="submit" class="btn btn-primary">Save Item</button>
|
|
</div>
|
|
<input type="hidden" id="shopEditId" value="">
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="page-moderation" class="page hidden">
|
|
<div class="page-header">
|
|
<h1 class="page-title"><span class="text-gradient">Moderation</span> Dashboard</h1>
|
|
<p class="page-subtitle">View and manage warnings, bans, and moderation actions</p>
|
|
</div>
|
|
|
|
<div class="card" style="margin-bottom:1.5rem">
|
|
<div class="card-header">
|
|
<h3 class="card-title">User Search</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<div style="display:flex;gap:1rem;flex-wrap:wrap">
|
|
<input type="text" id="modSearchInput" class="form-input" placeholder="Search by username or user ID..." style="flex:1;min-width:200px">
|
|
<button class="btn btn-primary" onclick="searchModUser()">Search</button>
|
|
</div>
|
|
<div id="modSearchResults" style="margin-top:1rem"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stats-grid" style="margin-bottom:1.5rem">
|
|
<div class="stat-card">
|
|
<div class="stat-label">Total Warnings</div>
|
|
<div class="stat-value" id="modTotalWarnings">-</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Active Bans</div>
|
|
<div class="stat-value" id="modActiveBans">-</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Recent Actions</div>
|
|
<div class="stat-value" id="modRecentActions">-</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tabs" style="margin-bottom:1.5rem">
|
|
<div class="tab active" data-mod-tab="mod-warnings">Warnings</div>
|
|
<div class="tab" data-mod-tab="mod-bans">Bans</div>
|
|
<div class="tab" data-mod-tab="mod-activity">Activity Feed</div>
|
|
<div class="tab" data-mod-tab="mod-bulk">Bulk Actions</div>
|
|
</div>
|
|
|
|
<div id="mod-warnings" class="mod-section">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Server Warnings</h3>
|
|
</div>
|
|
<div class="table-container">
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>User</th>
|
|
<th>Reason</th>
|
|
<th>Moderator</th>
|
|
<th>Date</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="modWarningsList">
|
|
<tr><td colspan="5" class="empty-state">Loading warnings...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="mod-bans" class="mod-section hidden">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Server Bans</h3>
|
|
</div>
|
|
<div class="table-container">
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>User</th>
|
|
<th>Reason</th>
|
|
<th>Moderator</th>
|
|
<th>Date</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="modBansList">
|
|
<tr><td colspan="4" class="empty-state">Loading bans...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="mod-activity" class="mod-section hidden">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Recent Activity Feed</h3>
|
|
</div>
|
|
<div class="card-body" id="modActivityFeed">
|
|
<div class="empty-state">Loading activity...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="mod-bulk" class="mod-section hidden">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Bulk Moderation Actions</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<p style="color:var(--text-secondary);margin-bottom:1.5rem">
|
|
Perform moderation actions on multiple users at once. Enter user IDs separated by spaces, commas, or newlines.
|
|
</p>
|
|
|
|
<div class="form-group">
|
|
<label for="bulkAction">Action Type</label>
|
|
<select id="bulkAction" class="form-input" onchange="updateBulkActionForm()">
|
|
<option value="ban">Ban Users</option>
|
|
<option value="kick">Kick Users</option>
|
|
<option value="timeout">Timeout Users</option>
|
|
<option value="warn">Warn Users</option>
|
|
<option value="remove_timeout">Remove Timeout</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="bulkUserIds">User IDs (max 25)</label>
|
|
<textarea id="bulkUserIds" class="form-input" rows="4" placeholder="123456789012345678 234567890123456789 345678901234567890"></textarea>
|
|
<small style="color:var(--text-secondary)">Enter one user ID per line, or separate with spaces/commas</small>
|
|
</div>
|
|
|
|
<div id="bulkTimeoutDuration" class="form-group" style="display:none">
|
|
<label for="bulkDuration">Timeout Duration</label>
|
|
<select id="bulkDuration" class="form-input">
|
|
<option value="5m">5 minutes</option>
|
|
<option value="10m">10 minutes</option>
|
|
<option value="30m">30 minutes</option>
|
|
<option value="1h" selected>1 hour</option>
|
|
<option value="6h">6 hours</option>
|
|
<option value="12h">12 hours</option>
|
|
<option value="1d">1 day</option>
|
|
<option value="3d">3 days</option>
|
|
<option value="1w">1 week</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div id="bulkReasonGroup" class="form-group">
|
|
<label for="bulkReason">Reason</label>
|
|
<input type="text" id="bulkReason" class="form-input" placeholder="Reason for action..." maxlength="500">
|
|
</div>
|
|
|
|
<div id="bulkDeleteDays" class="form-group" style="display:none">
|
|
<label for="bulkBanDeleteDays">Delete Message History</label>
|
|
<select id="bulkBanDeleteDays" class="form-input">
|
|
<option value="0">Don't delete messages</option>
|
|
<option value="1">1 day</option>
|
|
<option value="3">3 days</option>
|
|
<option value="7">7 days</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div style="display:flex;gap:1rem;align-items:center;margin-top:1.5rem">
|
|
<button class="btn btn-danger" onclick="executeBulkAction()" id="bulkActionBtn">Execute Bulk Ban</button>
|
|
<span id="bulkActionStatus"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card" style="margin-top:1.5rem">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Results</h3>
|
|
</div>
|
|
<div class="card-body" id="bulkActionResults">
|
|
<div class="empty-state">No bulk actions executed yet</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="page-analytics" class="page hidden">
|
|
<div class="page-header">
|
|
<h1 class="page-title"><span class="text-gradient">Analytics</span></h1>
|
|
<p class="page-subtitle">Server activity insights and statistics</p>
|
|
</div>
|
|
|
|
<div class="stats-grid" style="margin-bottom:1.5rem">
|
|
<div class="stat-card">
|
|
<div class="stat-label">Messages Today</div>
|
|
<div class="stat-value" id="analyticsMessagesToday">-</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Active Users (7d)</div>
|
|
<div class="stat-value" id="analyticsActiveUsers">-</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">XP Earned Today</div>
|
|
<div class="stat-value" id="analyticsXpToday">-</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Commands Used</div>
|
|
<div class="stat-value" id="analyticsCommandsUsed">-</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card" style="margin-bottom:1.5rem">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Activity Over Time (Last 7 Days)</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<div id="activityChart" style="height:200px;display:flex;align-items:flex-end;gap:0.5rem;padding:1rem 0">
|
|
<div class="empty-state" style="width:100%">Loading chart...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:1.5rem">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Top XP Earners (Today)</h3>
|
|
</div>
|
|
<div class="card-body" id="analyticsTopEarners">
|
|
<div class="empty-state">Loading...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Most Active Channels</h3>
|
|
</div>
|
|
<div class="card-body" id="analyticsTopChannels">
|
|
<div class="empty-state">Loading...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Command Usage</h3>
|
|
</div>
|
|
<div class="card-body" id="analyticsCommandStats">
|
|
<div class="empty-state">Loading...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="page-activity-roles" class="page hidden">
|
|
<div class="page-header">
|
|
<h1 class="page-title"><span class="text-gradient">Activity</span> Roles</h1>
|
|
<p class="page-subtitle">Automatically award roles when users reach activity milestones</p>
|
|
</div>
|
|
|
|
<div class="card" style="margin-bottom:1.5rem">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Add Activity Role</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="form-grid">
|
|
<div class="form-group">
|
|
<label>Role to Award</label>
|
|
<select id="activityRoleSelect" class="form-input">
|
|
<option value="">Select a role...</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Milestone Type</label>
|
|
<select id="activityMilestoneType" class="form-input">
|
|
<option value="messages">Messages Sent</option>
|
|
<option value="voice_hours">Voice Hours</option>
|
|
<option value="daily_streak">Daily Streak (Days)</option>
|
|
<option value="reactions_given">Reactions Given</option>
|
|
<option value="reactions_received">Reactions Received</option>
|
|
<option value="commands_used">Commands Used</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Milestone Value</label>
|
|
<input type="number" id="activityMilestoneValue" class="form-input" min="1" placeholder="e.g. 100">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Stack Roles</label>
|
|
<select id="activityStackRoles" class="form-input">
|
|
<option value="true">Yes - Keep previous roles</option>
|
|
<option value="false">No - Replace previous roles</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div style="margin-top:1rem">
|
|
<button class="btn btn-primary" onclick="addActivityRole()">Add Activity Role</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Configured Activity Roles</h3>
|
|
</div>
|
|
<div class="card-body" id="activityRolesList">
|
|
<div class="empty-state">
|
|
<div class="empty-state-icon">🎯</div>
|
|
<p>No activity roles configured</p>
|
|
<p style="font-size:0.875rem;color:var(--muted);margin-top:0.5rem">Add roles above to automatically reward active members</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="page-cooldowns" class="page hidden">
|
|
<div class="page-header">
|
|
<h1 class="page-title">Command <span class="text-gradient">Cooldowns</span></h1>
|
|
<p class="page-subtitle">Customize how often members can use specific commands</p>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Cooldown Settings</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<p style="color:var(--muted);margin-bottom:1.5rem;font-size:0.9rem">
|
|
Set custom cooldowns for commands. Use 0 for no cooldown, or click Reset to use the default value.
|
|
</p>
|
|
<div id="cooldownsList">
|
|
<div class="empty-state">
|
|
<div class="loading-spinner"></div>
|
|
<p style="margin-top:1rem">Loading cooldowns...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="page-federation" class="page hidden">
|
|
<div class="page-header">
|
|
<h1 class="page-title"><span class="text-gradient">Federation</span> Management</h1>
|
|
<p class="page-subtitle">Manage your federation membership, view bans, and applications</p>
|
|
</div>
|
|
|
|
<div class="stats-grid" id="fedStatsGrid">
|
|
<div class="stat-card">
|
|
<div class="stat-label">Member Servers</div>
|
|
<div class="stat-value" id="fedTotalServers">-</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Active Bans</div>
|
|
<div class="stat-value" id="fedActiveBans">-</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Pending Applications</div>
|
|
<div class="stat-value" id="fedPendingApps">-</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tabs" style="margin-bottom:1.5rem">
|
|
<div class="tab active" data-fed-tab="fed-servers">Servers</div>
|
|
<div class="tab" data-fed-tab="fed-bans">Global Bans</div>
|
|
<div class="tab" data-fed-tab="fed-applications">Applications</div>
|
|
<div class="tab" data-fed-tab="fed-leaderboard">Leaderboard</div>
|
|
</div>
|
|
|
|
<div id="fed-servers" class="fed-section">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Federation Servers</h3>
|
|
</div>
|
|
<div class="card-body" id="fedServerGrid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:1rem">
|
|
<div class="empty-state">Loading servers...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="fed-bans" class="fed-section hidden">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Global Ban List</h3>
|
|
</div>
|
|
<div class="table-container">
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>User</th>
|
|
<th>Severity</th>
|
|
<th>Reason</th>
|
|
<th>Date</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="fedBanList">
|
|
<tr><td colspan="4" class="empty-state">Loading bans...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="fed-applications" class="fed-section hidden">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Pending Applications</h3>
|
|
</div>
|
|
<div class="table-container">
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Server</th>
|
|
<th>Category</th>
|
|
<th>Members</th>
|
|
<th>Status</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="fedAppList">
|
|
<tr><td colspan="5" class="empty-state">Loading applications...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="fed-leaderboard" class="fed-section hidden">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Federation Reputation Leaders</h3>
|
|
</div>
|
|
<div class="card-body" id="fedLeaderboardList">
|
|
<div class="empty-state">Loading leaderboard...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card" style="margin-top:2rem">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Upgrade Protection</h3>
|
|
</div>
|
|
<div class="card-body" style="display:flex;gap:1rem;flex-wrap:wrap">
|
|
<a href="/pricing" class="btn btn-primary">View Pricing Plans</a>
|
|
<a href="/federation" class="btn btn-secondary">Learn About Federation</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="page-backups" class="page hidden">
|
|
<div class="page-header">
|
|
<h1 class="page-title">Server <span class="text-gradient">Backups</span></h1>
|
|
<p class="page-subtitle">Create and manage configuration backups for your server</p>
|
|
</div>
|
|
|
|
<div class="stats-grid" id="backupStatsGrid">
|
|
<div class="stat-card">
|
|
<div class="stat-label">Total Backups</div>
|
|
<div class="stat-value" id="backupTotalCount">-</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Auto Backups</div>
|
|
<div class="stat-value" id="backupAutoCount">-</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Last Backup</div>
|
|
<div class="stat-value" id="backupLastTime">-</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header" style="display:flex;justify-content:space-between;align-items:center">
|
|
<h3 class="card-title">Create Backup</h3>
|
|
<button class="btn btn-primary" onclick="createBackup()">Create New Backup</button>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="form-group" style="margin-bottom:1rem">
|
|
<label class="form-label">Backup Name</label>
|
|
<input type="text" class="form-input" id="backupName" placeholder="My Server Backup">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Description (optional)</label>
|
|
<input type="text" class="form-input" id="backupDescription" placeholder="Before major changes...">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card" style="margin-top:1.5rem">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Automatic Backups</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<div style="display:flex;flex-wrap:wrap;gap:1.5rem;align-items:end">
|
|
<div class="form-group" style="flex:1;min-width:150px">
|
|
<label class="form-label">Auto Backup</label>
|
|
<select class="form-select" id="backupAutoEnabled">
|
|
<option value="false">Disabled</option>
|
|
<option value="true">Enabled</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group" style="flex:1;min-width:150px">
|
|
<label class="form-label">Interval (hours)</label>
|
|
<input type="number" class="form-input" id="backupInterval" value="24" min="1" max="168">
|
|
</div>
|
|
<div class="form-group" style="flex:1;min-width:150px">
|
|
<label class="form-label">Keep Backups</label>
|
|
<input type="number" class="form-input" id="backupMaxKeep" value="7" min="1" max="30">
|
|
</div>
|
|
<button class="btn btn-primary" onclick="saveBackupSettings()">Save Settings</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card" style="margin-top:1.5rem">
|
|
<div class="card-header">
|
|
<h3 class="card-title">Available Backups</h3>
|
|
</div>
|
|
<div class="table-container">
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Type</th>
|
|
<th>Roles</th>
|
|
<th>Channels</th>
|
|
<th>Size</th>
|
|
<th>Created</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="backupsList">
|
|
<tr><td colspan="7" class="empty-state">Loading backups...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="backupModal" class="modal hidden">
|
|
<div class="modal-backdrop" onclick="closeBackupModal()"></div>
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h3 id="backupModalTitle">Backup Details</h3>
|
|
<button class="modal-close" onclick="closeBackupModal()">×</button>
|
|
</div>
|
|
<div class="modal-body" id="backupModalBody">
|
|
Loading...
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
<script>
|
|
let currentUser = null;
|
|
let currentGuild = null;
|
|
let currentPage = 'profile';
|
|
|
|
async function init() {
|
|
try {
|
|
const res = await fetch('/api/me');
|
|
if (!res.ok) {
|
|
document.getElementById('loginPrompt').classList.remove('hidden');
|
|
return;
|
|
}
|
|
|
|
currentUser = await res.json();
|
|
document.getElementById('loginPrompt').classList.add('hidden');
|
|
document.getElementById('app').classList.remove('hidden');
|
|
|
|
document.getElementById('userAvatar').src = currentUser.avatarUrl;
|
|
document.getElementById('userName').textContent = currentUser.globalName || currentUser.username;
|
|
|
|
populateGuildCards(currentUser.guilds);
|
|
|
|
if (currentUser.guilds.length > 0) {
|
|
selectGuild(currentUser.guilds[0]);
|
|
}
|
|
|
|
document.addEventListener('click', (e) => {
|
|
const selector = document.querySelector('.guild-selector');
|
|
if (!selector.contains(e.target)) {
|
|
closeGuildDropdown();
|
|
}
|
|
});
|
|
|
|
document.querySelectorAll('.nav-item').forEach(item => {
|
|
item.addEventListener('click', () => {
|
|
document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active'));
|
|
item.classList.add('active');
|
|
currentPage = item.dataset.page;
|
|
showPage(currentPage);
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll('[data-lb-type]').forEach(tab => {
|
|
tab.addEventListener('click', () => {
|
|
document.querySelectorAll('[data-lb-type]').forEach(t => t.classList.remove('active'));
|
|
tab.classList.add('active');
|
|
loadLeaderboard(tab.dataset.lbType);
|
|
});
|
|
});
|
|
|
|
await loadProfile();
|
|
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const pageParam = urlParams.get('page');
|
|
if (pageParam) {
|
|
const navItem = document.querySelector(`.nav-item[data-page="${pageParam}"]`);
|
|
if (navItem) {
|
|
document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active'));
|
|
navItem.classList.add('active');
|
|
currentPage = pageParam;
|
|
showPage(pageParam);
|
|
}
|
|
}
|
|
|
|
} catch (e) {
|
|
console.error('Init error:', e);
|
|
document.getElementById('loginPrompt').classList.remove('hidden');
|
|
}
|
|
}
|
|
|
|
function populateGuildCards(guilds) {
|
|
const dropdown = document.getElementById('guildDropdown');
|
|
dropdown.innerHTML = '';
|
|
|
|
if (guilds.length === 0) {
|
|
dropdown.innerHTML = '<div style="padding: 1rem; text-align: center; color: var(--muted);">No servers found</div>';
|
|
return;
|
|
}
|
|
|
|
guilds.forEach(g => {
|
|
const card = document.createElement('div');
|
|
card.className = 'guild-card';
|
|
card.dataset.guildId = g.id;
|
|
card.dataset.isAdmin = g.isAdmin;
|
|
card.onclick = () => selectGuild(g);
|
|
|
|
const iconContent = g.icon
|
|
? `<img src="${g.icon}" alt="${g.name}">`
|
|
: g.name.charAt(0).toUpperCase();
|
|
|
|
let statusHtml = '';
|
|
if (g.isAdmin) {
|
|
statusHtml = '<span class="guild-admin-badge">Admin</span>';
|
|
}
|
|
if (g.memberCount) {
|
|
statusHtml += `<span class="guild-member-count">${g.memberCount.toLocaleString()} members</span>`;
|
|
}
|
|
|
|
card.innerHTML = `
|
|
<div class="guild-icon">${iconContent}</div>
|
|
<div class="guild-info">
|
|
<div class="guild-name">${escapeHtml(g.name)}</div>
|
|
<div class="guild-status">${statusHtml}</div>
|
|
</div>
|
|
<svg class="guild-check" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3">
|
|
<path d="M20 6L9 17l-5-5"/>
|
|
</svg>
|
|
`;
|
|
|
|
dropdown.appendChild(card);
|
|
});
|
|
}
|
|
|
|
function selectGuild(guild) {
|
|
currentGuild = guild.id;
|
|
|
|
document.querySelectorAll('.guild-card').forEach(c => {
|
|
c.classList.toggle('active', c.dataset.guildId === guild.id);
|
|
});
|
|
|
|
const iconEl = document.getElementById('selectedGuildIcon');
|
|
if (guild.icon) {
|
|
iconEl.innerHTML = `<img src="${guild.icon}" alt="${guild.name}">`;
|
|
} else {
|
|
iconEl.innerHTML = guild.name.charAt(0).toUpperCase();
|
|
}
|
|
|
|
document.getElementById('selectedGuildName').textContent = guild.name;
|
|
document.getElementById('adminSection').style.display = guild.isAdmin ? 'block' : 'none';
|
|
|
|
closeGuildDropdown();
|
|
loadPageData();
|
|
}
|
|
|
|
function toggleGuildDropdown() {
|
|
const header = document.getElementById('guildSelectorHeader');
|
|
const dropdown = document.getElementById('guildDropdown');
|
|
const isOpen = dropdown.classList.contains('open');
|
|
|
|
header.classList.toggle('open', !isOpen);
|
|
dropdown.classList.toggle('open', !isOpen);
|
|
}
|
|
|
|
function closeGuildDropdown() {
|
|
document.getElementById('guildSelectorHeader').classList.remove('open');
|
|
document.getElementById('guildDropdown').classList.remove('open');
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function showPage(page) {
|
|
document.querySelectorAll('.page').forEach(p => p.classList.add('hidden'));
|
|
document.getElementById('page-' + page)?.classList.remove('hidden');
|
|
loadPageData();
|
|
}
|
|
|
|
async function loadPageData() {
|
|
if (!currentGuild) return;
|
|
|
|
switch (currentPage) {
|
|
case 'profile': await loadProfile(); break;
|
|
case 'leaderboard': await loadLeaderboard('all'); break;
|
|
case 'achievements': await loadAchievements(); break;
|
|
case 'quests': await loadQuests(); break;
|
|
case 'shop': await loadShop(); break;
|
|
case 'inventory': await loadInventory(); break;
|
|
case 'titles': await loadTitles(); break;
|
|
case 'coins': await loadCoins(); break;
|
|
case 'admin-xp': await loadXpConfig(); break;
|
|
case 'admin-quests': await loadAdminQuests(); break;
|
|
case 'admin-achievements': await loadAdminAchievements(); break;
|
|
case 'admin-shop': await loadAdminShop(); break;
|
|
case 'federation': loadFederationData(); break;
|
|
case 'moderation': loadModerationData(); break;
|
|
case 'analytics': loadAnalyticsData(); break;
|
|
case 'activity-roles': loadActivityRoles(); break;
|
|
case 'cooldowns': loadCooldowns(); break;
|
|
case 'backups': loadBackups(); break;
|
|
}
|
|
}
|
|
|
|
async function loadProfile() {
|
|
if (!currentUser) return;
|
|
|
|
try {
|
|
const [profileRes, statsRes] = await Promise.all([
|
|
fetch('/api/profile/' + currentUser.id),
|
|
currentGuild ? fetch('/api/stats/' + currentUser.id + '/' + currentGuild) : null
|
|
]);
|
|
|
|
const profileData = await profileRes.json();
|
|
const statsData = statsRes ? await statsRes.json() : { stats: null };
|
|
|
|
if (profileData.linked && profileData.profile) {
|
|
const p = profileData.profile;
|
|
const xp = p.xp || 0;
|
|
const level = Math.floor(Math.sqrt(xp / 100));
|
|
const nextLevel = level + 1;
|
|
const currentLevelXp = level * level * 100;
|
|
const nextLevelXp = nextLevel * nextLevel * 100;
|
|
const progress = ((xp - currentLevelXp) / (nextLevelXp - currentLevelXp)) * 100;
|
|
|
|
document.getElementById('statXp').textContent = xp.toLocaleString();
|
|
document.getElementById('statLevel').textContent = level;
|
|
document.getElementById('statPrestige').textContent = 'Prestige ' + (p.prestigeLevel || 0);
|
|
document.getElementById('statStreak').textContent = p.dailyStreak || 0;
|
|
document.getElementById('userLevel').textContent = 'Level ' + level;
|
|
|
|
document.getElementById('currentLevel').textContent = level;
|
|
document.getElementById('nextLevel').textContent = nextLevel;
|
|
document.getElementById('xpCurrent').textContent = (xp - currentLevelXp).toLocaleString();
|
|
document.getElementById('xpRequired').textContent = (nextLevelXp - currentLevelXp).toLocaleString();
|
|
document.getElementById('levelProgress').style.width = progress + '%';
|
|
}
|
|
|
|
if (statsData.stats) {
|
|
const s = statsData.stats;
|
|
document.getElementById('statMessages').textContent = (s.messages_sent || 0).toLocaleString();
|
|
const voiceMinutes = s.voice_minutes || 0;
|
|
document.getElementById('statVoice').textContent = Math.floor(voiceMinutes / 60) + 'h';
|
|
}
|
|
|
|
// Load coins for profile display
|
|
if (currentGuild) {
|
|
try {
|
|
const coinsRes = await fetch('/api/guild/' + currentGuild + '/coins');
|
|
if (coinsRes.ok) {
|
|
const coinsData = await coinsRes.json();
|
|
const statCoinsEl = document.getElementById('statCoins');
|
|
const statCoinLabelEl = document.getElementById('statCoinLabel');
|
|
if (statCoinsEl) statCoinsEl.textContent = (coinsData.coins || 0).toLocaleString();
|
|
if (statCoinLabelEl) statCoinLabelEl.textContent = coinsData.coinName || 'Coins';
|
|
}
|
|
} catch (ce) {
|
|
// Silently skip on error
|
|
}
|
|
}
|
|
|
|
} catch (e) {
|
|
console.error('Profile load error:', e);
|
|
}
|
|
}
|
|
|
|
async function loadLeaderboard(type) {
|
|
if (!currentGuild) return;
|
|
|
|
const container = document.getElementById('leaderboardList');
|
|
container.innerHTML = '<div style="display:flex;justify-content:center;padding:3rem"><div class="loading-spinner"></div></div>';
|
|
|
|
try {
|
|
const res = await fetch('/api/leaderboard/' + currentGuild + '?type=' + type + '&limit=25');
|
|
const data = await res.json();
|
|
|
|
if (!data.leaderboard || data.leaderboard.length === 0) {
|
|
container.innerHTML = '<div class="empty-state"><div class="empty-state-icon">🏆</div><p>No leaderboard data yet</p></div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = data.leaderboard.map((entry, i) => `
|
|
<div class="leaderboard-item ${i < 3 ? 'top-3' : ''}">
|
|
<div class="leaderboard-rank">${i + 1}</div>
|
|
<img class="leaderboard-avatar" src="${entry.user_profiles?.avatar_url || ''}" alt="">
|
|
<div class="leaderboard-name">${entry.user_profiles?.username || 'Unknown'}</div>
|
|
<div class="leaderboard-xp">${(entry.xp || 0).toLocaleString()} XP</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
} catch (e) {
|
|
container.innerHTML = '<div class="empty-state"><p>Failed to load leaderboard</p></div>';
|
|
}
|
|
}
|
|
|
|
async function loadAchievements() {
|
|
if (!currentGuild) return;
|
|
|
|
const container = document.getElementById('achievementGrid');
|
|
container.innerHTML = '<div style="display:flex;justify-content:center;padding:3rem;grid-column:1/-1"><div class="loading-spinner"></div></div>';
|
|
|
|
try {
|
|
const res = await fetch('/api/achievements/' + currentGuild);
|
|
const data = await res.json();
|
|
|
|
if (!data.achievements || data.achievements.length === 0) {
|
|
container.innerHTML = '<div class="empty-state" style="grid-column:1/-1"><div class="empty-state-icon">🎖️</div><p>No achievements configured for this server</p></div>';
|
|
return;
|
|
}
|
|
|
|
const earnedIds = new Set(data.userAchievements?.map(a => a.achievement_id) || []);
|
|
document.getElementById('statAchievements').textContent = earnedIds.size;
|
|
|
|
container.innerHTML = data.achievements.map(a => `
|
|
<div class="achievement-item ${earnedIds.has(a.id) ? '' : 'locked'}">
|
|
<div class="achievement-icon">${a.icon || '🏅'}</div>
|
|
<div class="achievement-info">
|
|
<h4>${a.name}</h4>
|
|
<p>${a.description || ''}</p>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
} catch (e) {
|
|
container.innerHTML = '<div class="empty-state" style="grid-column:1/-1"><p>Failed to load achievements</p></div>';
|
|
}
|
|
}
|
|
|
|
async function loadQuests() {
|
|
if (!currentGuild) return;
|
|
|
|
const container = document.getElementById('questList');
|
|
container.innerHTML = '<div style="display:flex;justify-content:center;padding:3rem"><div class="loading-spinner"></div></div>';
|
|
|
|
try {
|
|
const res = await fetch('/api/quests/' + currentGuild);
|
|
const data = await res.json();
|
|
|
|
if (!data.quests || data.quests.length === 0) {
|
|
container.innerHTML = '<div class="empty-state"><div class="empty-state-icon">🎯</div><p>No active quests</p></div>';
|
|
return;
|
|
}
|
|
|
|
const userQuestMap = {};
|
|
(data.userQuests || []).forEach(uq => userQuestMap[uq.quest_id] = uq);
|
|
|
|
container.innerHTML = data.quests.map(q => {
|
|
const uq = userQuestMap[q.id];
|
|
const progress = uq?.progress || 0;
|
|
const pct = Math.min(100, (progress / q.target_value) * 100);
|
|
|
|
return `
|
|
<div class="quest-item">
|
|
<div class="quest-info">
|
|
<div class="quest-name">${q.name}</div>
|
|
<div class="quest-desc">${q.description || ''}</div>
|
|
<div class="progress-bar" style="margin-top:0.5rem">
|
|
<div class="progress-fill" style="width:${pct}%"></div>
|
|
</div>
|
|
</div>
|
|
<div class="quest-progress">
|
|
<div class="quest-progress-text">${progress}/${q.target_value}</div>
|
|
<div style="font-size:0.75rem;color:var(--muted)">${q.xp_reward} XP</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
|
|
} catch (e) {
|
|
container.innerHTML = '<div class="empty-state"><p>Failed to load quests</p></div>';
|
|
}
|
|
}
|
|
|
|
async function loadShop() {
|
|
if (!currentGuild) return;
|
|
|
|
const container = document.getElementById('shopGrid');
|
|
container.innerHTML = '<div style="display:flex;justify-content:center;padding:3rem;grid-column:1/-1"><div class="loading-spinner"></div></div>';
|
|
|
|
try {
|
|
const res = await fetch('/api/shop/' + currentGuild);
|
|
const data = await res.json();
|
|
|
|
if (!data.items || data.items.length === 0) {
|
|
container.innerHTML = '<div class="empty-state" style="grid-column:1/-1"><div class="empty-state-icon">🛒</div><p>No shop items available</p></div>';
|
|
return;
|
|
}
|
|
|
|
const icons = { title: '📛', badge: '🏅', color: '🎨', role: '👑', xp_boost: '⚡', booster: '⚡', background: '🎨', special: '✨', custom: '✨' };
|
|
|
|
container.innerHTML = data.items.map(item => `
|
|
<div class="shop-item">
|
|
<div class="shop-icon">${icons[item.item_type] || '🎁'}</div>
|
|
<div class="shop-name">${item.name}</div>
|
|
<div class="shop-price">${item.price.toLocaleString()} XP</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
} catch (e) {
|
|
container.innerHTML = '<div class="empty-state" style="grid-column:1/-1"><p>Failed to load shop</p></div>';
|
|
}
|
|
}
|
|
|
|
async function loadInventory() {
|
|
if (!currentGuild) return;
|
|
|
|
const container = document.getElementById('inventoryGrid');
|
|
container.innerHTML = '<div style="display:flex;justify-content:center;padding:3rem;grid-column:1/-1"><div class="loading-spinner"></div></div>';
|
|
|
|
try {
|
|
const res = await fetch('/api/inventory/' + currentGuild);
|
|
const data = await res.json();
|
|
|
|
if (!data.inventory || data.inventory.length === 0) {
|
|
container.innerHTML = '<div class="empty-state" style="grid-column:1/-1"><div class="empty-state-icon">🎒</div><p>Your inventory is empty</p></div>';
|
|
return;
|
|
}
|
|
|
|
const icons = { title: '📛', badge: '🏅', color: '🎨', role: '👑', xp_boost: '⚡', booster: '⚡', background: '🎨', special: '✨', custom: '✨' };
|
|
|
|
container.innerHTML = data.inventory.map(item => `
|
|
<div class="shop-item ${item.equipped ? 'equipped' : ''}">
|
|
<div class="shop-icon">${icons[item.shop_items?.item_type] || '🎁'}</div>
|
|
<div class="shop-name">${item.shop_items?.name || 'Unknown Item'}</div>
|
|
<div style="font-size:0.75rem;color:var(--muted)">${item.equipped ? 'Equipped' : ''}</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
} catch (e) {
|
|
container.innerHTML = '<div class="empty-state" style="grid-column:1/-1"><p>Failed to load inventory</p></div>';
|
|
}
|
|
}
|
|
|
|
async function loadXpConfig() {
|
|
if (!currentGuild) return;
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/config');
|
|
if (!res.ok) {
|
|
if (res.status === 403) {
|
|
showSaveStatus('xpSaveStatus', 'You do not have admin access to this server', 'error');
|
|
}
|
|
return;
|
|
}
|
|
|
|
const data = await res.json();
|
|
const config = data.xpConfig || {};
|
|
|
|
document.getElementById('xpMsgMin').value = config.message_xp_min ?? 15;
|
|
document.getElementById('xpMsgMax').value = config.message_xp_max ?? 25;
|
|
document.getElementById('xpMsgCooldown').value = config.message_cooldown ?? 60;
|
|
document.getElementById('xpReaction').value = config.reaction_xp ?? 5;
|
|
document.getElementById('xpVoice').value = config.voice_xp_per_minute ?? 2;
|
|
document.getElementById('xpDaily').value = config.daily_xp ?? 100;
|
|
document.getElementById('xpBase').value = config.xp_base ?? 100;
|
|
document.getElementById('levelUpChannel').value = config.levelup_channel_id || '';
|
|
document.getElementById('levelUpEnabled').checked = config.levelup_enabled !== false;
|
|
document.getElementById('levelUpDm').checked = config.levelup_dm === true;
|
|
document.getElementById('xpMultiplier').value = config.xp_multiplier ?? 1.0;
|
|
document.getElementById('weekendMultiplier').value = config.weekend_multiplier ?? 1.5;
|
|
|
|
} catch (e) {
|
|
console.error('Failed to load XP config:', e);
|
|
}
|
|
}
|
|
|
|
async function saveXpConfig(e) {
|
|
e.preventDefault();
|
|
|
|
if (!currentGuild) return;
|
|
|
|
const form = document.getElementById('xpSettingsForm');
|
|
const data = {
|
|
message_xp_min: parseInt(form.xpMsgMin.value) || 15,
|
|
message_xp_max: parseInt(form.xpMsgMax.value) || 25,
|
|
message_cooldown: parseInt(form.xpMsgCooldown.value) || 60,
|
|
reaction_xp: parseInt(form.xpReaction.value) || 5,
|
|
voice_xp_per_minute: parseInt(form.xpVoice.value) || 2,
|
|
daily_xp: parseInt(form.xpDaily.value) || 100,
|
|
xp_base: parseInt(form.xpBase.value) || 100,
|
|
levelup_channel_id: form.levelUpChannel.value || null,
|
|
levelup_enabled: form.levelUpEnabled.checked,
|
|
levelup_dm: form.levelUpDm.checked,
|
|
xp_multiplier: parseFloat(form.xpMultiplier.value) || 1.0,
|
|
weekend_multiplier: parseFloat(form.weekendMultiplier.value) || 1.5
|
|
};
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/xp-config', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
|
|
if (res.ok) {
|
|
showSaveStatus('xpSaveStatus', 'Settings saved successfully!', 'success');
|
|
} else {
|
|
const err = await res.json();
|
|
showSaveStatus('xpSaveStatus', err.error || 'Failed to save settings', 'error');
|
|
}
|
|
} catch (e) {
|
|
showSaveStatus('xpSaveStatus', 'Failed to save settings', 'error');
|
|
}
|
|
}
|
|
|
|
function showSaveStatus(elementId, message, type) {
|
|
const el = document.getElementById(elementId);
|
|
el.textContent = message;
|
|
el.className = 'save-status ' + type;
|
|
el.classList.remove('hidden');
|
|
|
|
setTimeout(() => {
|
|
el.classList.add('hidden');
|
|
}, 5000);
|
|
}
|
|
|
|
const QUEST_TYPES = { daily: { emoji: '☀️', name: 'Daily' }, weekly: { emoji: '📅', name: 'Weekly' }, special: { emoji: '⭐', name: 'Special' } };
|
|
const OBJECTIVES = { messages: '💬', reactions: '😄', voice_minutes: '🎙️', commands: '⚡', daily_claims: '🎁', level_ups: '📈', xp_earned: '✨' };
|
|
|
|
async function loadAdminQuests() {
|
|
if (!currentGuild) return;
|
|
|
|
const container = document.getElementById('adminQuestList');
|
|
container.innerHTML = '<div style="display:flex;justify-content:center;padding:3rem"><div class="loading-spinner"></div></div>';
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/quests');
|
|
if (!res.ok) {
|
|
container.innerHTML = '<div class="empty-state"><p>Failed to load quests</p></div>';
|
|
return;
|
|
}
|
|
|
|
const data = await res.json();
|
|
|
|
if (!data.quests || data.quests.length === 0) {
|
|
container.innerHTML = '<div class="empty-state"><div class="empty-state-icon">🎯</div><p>No quests configured yet</p></div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = data.quests.map(q => `
|
|
<div class="item-row">
|
|
<div style="font-size:1.5rem">${QUEST_TYPES[q.quest_type]?.emoji || '🎯'}</div>
|
|
<div class="item-info">
|
|
<div class="item-name">
|
|
${q.name}
|
|
<span class="item-badge ${q.active ? 'active' : 'inactive'}">${q.active ? 'Active' : 'Inactive'}</span>
|
|
</div>
|
|
<div class="item-desc">${OBJECTIVES[q.objective] || ''} Target: ${q.target_value} | Reward: ${q.xp_reward} XP</div>
|
|
</div>
|
|
<div class="item-actions">
|
|
<button class="btn-icon" onclick="editQuest(${q.id})" title="Edit">✏️</button>
|
|
<button class="btn-icon danger" onclick="deleteQuest(${q.id})" title="Delete">🗑️</button>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
window.adminQuests = data.quests;
|
|
|
|
} catch (e) {
|
|
container.innerHTML = '<div class="empty-state"><p>Failed to load quests</p></div>';
|
|
}
|
|
}
|
|
|
|
function openQuestModal(quest = null) {
|
|
document.getElementById('questModalTitle').textContent = quest ? 'Edit Quest' : 'Create Quest';
|
|
document.getElementById('questEditId').value = quest?.id || '';
|
|
document.getElementById('questName').value = quest?.name || '';
|
|
document.getElementById('questDescription').value = quest?.description || '';
|
|
document.getElementById('questType').value = quest?.quest_type || 'daily';
|
|
document.getElementById('questObjective').value = quest?.objective || 'messages';
|
|
document.getElementById('questTarget').value = quest?.target_value || 10;
|
|
document.getElementById('questXpReward').value = quest?.xp_reward || 100;
|
|
document.getElementById('questDuration').value = quest?.duration_hours || 24;
|
|
document.getElementById('questRepeatable').checked = quest?.repeatable || false;
|
|
document.getElementById('questActive').checked = quest?.active !== false;
|
|
document.getElementById('questModal').classList.remove('hidden');
|
|
}
|
|
|
|
function closeQuestModal() {
|
|
document.getElementById('questModal').classList.add('hidden');
|
|
}
|
|
|
|
function editQuest(questId) {
|
|
const quest = window.adminQuests?.find(q => q.id === questId);
|
|
if (quest) openQuestModal(quest);
|
|
}
|
|
|
|
async function saveQuest(e) {
|
|
e.preventDefault();
|
|
|
|
if (!currentGuild) return;
|
|
|
|
const questId = document.getElementById('questEditId').value;
|
|
const data = {
|
|
name: document.getElementById('questName').value,
|
|
description: document.getElementById('questDescription').value,
|
|
quest_type: document.getElementById('questType').value,
|
|
objective: document.getElementById('questObjective').value,
|
|
target_value: parseInt(document.getElementById('questTarget').value),
|
|
xp_reward: parseInt(document.getElementById('questXpReward').value),
|
|
duration_hours: parseInt(document.getElementById('questDuration').value) || 0,
|
|
repeatable: document.getElementById('questRepeatable').checked,
|
|
active: document.getElementById('questActive').checked
|
|
};
|
|
|
|
try {
|
|
const url = questId
|
|
? '/api/guild/' + currentGuild + '/quests/' + questId
|
|
: '/api/guild/' + currentGuild + '/quests';
|
|
|
|
const res = await fetch(url, {
|
|
method: questId ? 'PUT' : 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
|
|
if (res.ok) {
|
|
closeQuestModal();
|
|
showSaveStatus('questSaveStatus', questId ? 'Quest updated!' : 'Quest created!', 'success');
|
|
await loadAdminQuests();
|
|
} else {
|
|
const err = await res.json();
|
|
showSaveStatus('questSaveStatus', err.error || 'Failed to save quest', 'error');
|
|
}
|
|
} catch (e) {
|
|
showSaveStatus('questSaveStatus', 'Failed to save quest', 'error');
|
|
}
|
|
}
|
|
|
|
async function deleteQuest(questId) {
|
|
if (!confirm('Are you sure you want to delete this quest?')) return;
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/quests/' + questId, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
if (res.ok) {
|
|
showSaveStatus('questSaveStatus', 'Quest deleted', 'success');
|
|
await loadAdminQuests();
|
|
} else {
|
|
showSaveStatus('questSaveStatus', 'Failed to delete quest', 'error');
|
|
}
|
|
} catch (e) {
|
|
showSaveStatus('questSaveStatus', 'Failed to delete quest', 'error');
|
|
}
|
|
}
|
|
|
|
const TRIGGER_TYPES = {
|
|
level: { emoji: '📈', name: 'Reach Level' },
|
|
prestige: { emoji: '👑', name: 'Reach Prestige' },
|
|
total_xp: { emoji: '✨', name: 'Total XP Earned' },
|
|
messages: { emoji: '💬', name: 'Messages Sent' },
|
|
reactions_given: { emoji: '😄', name: 'Reactions Given' },
|
|
reactions_received: { emoji: '❤️', name: 'Reactions Received' },
|
|
voice_minutes: { emoji: '🎙️', name: 'Voice Minutes' },
|
|
daily_streak: { emoji: '🔥', name: 'Daily Streak' },
|
|
commands_used: { emoji: '⚡', name: 'Commands Used' }
|
|
};
|
|
|
|
const ITEM_TYPES = {
|
|
badge: { emoji: '🏅', name: 'Badge' },
|
|
title: { emoji: '🏷️', name: 'Title' },
|
|
background: { emoji: '🎨', name: 'Background' },
|
|
booster: { emoji: '⚡', name: 'XP Booster' },
|
|
role: { emoji: '👑', name: 'Role' },
|
|
special: { emoji: '✨', name: 'Special' }
|
|
};
|
|
|
|
async function loadAdminAchievements() {
|
|
if (!currentGuild) return;
|
|
|
|
const container = document.getElementById('adminAchievementList');
|
|
container.innerHTML = '<div style="display:flex;justify-content:center;padding:3rem"><div class="loading-spinner"></div></div>';
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/achievements');
|
|
if (!res.ok) {
|
|
container.innerHTML = '<div class="empty-state"><p>Failed to load achievements</p></div>';
|
|
return;
|
|
}
|
|
|
|
const data = await res.json();
|
|
|
|
if (!data.achievements || data.achievements.length === 0) {
|
|
container.innerHTML = '<div class="empty-state"><div class="empty-state-icon">🏅</div><p>No achievements configured yet</p></div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = data.achievements.map(a => `
|
|
<div class="item-row">
|
|
<div style="font-size:1.5rem">${a.icon || '🏆'}</div>
|
|
<div class="item-info">
|
|
<div class="item-name">
|
|
${a.name}
|
|
${a.hidden ? '<span class="item-badge hidden">Hidden</span>' : ''}
|
|
</div>
|
|
<div class="item-desc">${TRIGGER_TYPES[a.trigger_type]?.name || a.trigger_type}: ${a.trigger_value} | Reward: ${a.reward_xp || 0} XP${a.reward_role_id ? ' + Role' : ''}</div>
|
|
</div>
|
|
<div class="item-actions">
|
|
<button class="btn-icon" onclick="editAchievement(${a.id})" title="Edit">✏️</button>
|
|
<button class="btn-icon danger" onclick="deleteAchievement(${a.id})" title="Delete">🗑️</button>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
window.adminAchievements = data.achievements;
|
|
|
|
} catch (e) {
|
|
container.innerHTML = '<div class="empty-state"><p>Failed to load achievements</p></div>';
|
|
}
|
|
}
|
|
|
|
function openAchievementModal(achievement = null) {
|
|
document.getElementById('achievementModalTitle').textContent = achievement ? 'Edit Achievement' : 'Create Achievement';
|
|
document.getElementById('achievementEditId').value = achievement?.id || '';
|
|
document.getElementById('achievementName').value = achievement?.name || '';
|
|
document.getElementById('achievementIcon').value = achievement?.icon || '🏆';
|
|
document.getElementById('achievementDescription').value = achievement?.description || '';
|
|
document.getElementById('achievementTrigger').value = achievement?.trigger_type || 'level';
|
|
document.getElementById('achievementValue').value = achievement?.trigger_value || 10;
|
|
document.getElementById('achievementXpReward').value = achievement?.reward_xp || 100;
|
|
document.getElementById('achievementRoleReward').value = achievement?.reward_role_id || '';
|
|
document.getElementById('achievementHidden').checked = achievement?.hidden || false;
|
|
document.getElementById('achievementModal').classList.remove('hidden');
|
|
}
|
|
|
|
function closeAchievementModal() {
|
|
document.getElementById('achievementModal').classList.add('hidden');
|
|
}
|
|
|
|
function editAchievement(achievementId) {
|
|
const achievement = window.adminAchievements?.find(a => a.id === achievementId);
|
|
if (achievement) openAchievementModal(achievement);
|
|
}
|
|
|
|
async function saveAchievement(e) {
|
|
e.preventDefault();
|
|
|
|
if (!currentGuild) return;
|
|
|
|
const achievementId = document.getElementById('achievementEditId').value;
|
|
const data = {
|
|
name: document.getElementById('achievementName').value,
|
|
icon: document.getElementById('achievementIcon').value || '🏆',
|
|
description: document.getElementById('achievementDescription').value,
|
|
trigger_type: document.getElementById('achievementTrigger').value,
|
|
trigger_value: parseInt(document.getElementById('achievementValue').value),
|
|
reward_xp: parseInt(document.getElementById('achievementXpReward').value) || 0,
|
|
reward_role_id: document.getElementById('achievementRoleReward').value || null,
|
|
hidden: document.getElementById('achievementHidden').checked
|
|
};
|
|
|
|
try {
|
|
const url = achievementId
|
|
? '/api/guild/' + currentGuild + '/achievements/' + achievementId
|
|
: '/api/guild/' + currentGuild + '/achievements';
|
|
|
|
const res = await fetch(url, {
|
|
method: achievementId ? 'PUT' : 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
|
|
if (res.ok) {
|
|
closeAchievementModal();
|
|
showSaveStatus('achievementSaveStatus', achievementId ? 'Achievement updated!' : 'Achievement created!', 'success');
|
|
await loadAdminAchievements();
|
|
} else {
|
|
const err = await res.json();
|
|
showSaveStatus('achievementSaveStatus', err.error || 'Failed to save achievement', 'error');
|
|
}
|
|
} catch (e) {
|
|
showSaveStatus('achievementSaveStatus', 'Failed to save achievement', 'error');
|
|
}
|
|
}
|
|
|
|
async function deleteAchievement(achievementId) {
|
|
if (!confirm('Are you sure you want to delete this achievement?')) return;
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/achievements/' + achievementId, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
if (res.ok) {
|
|
showSaveStatus('achievementSaveStatus', 'Achievement deleted', 'success');
|
|
await loadAdminAchievements();
|
|
} else {
|
|
showSaveStatus('achievementSaveStatus', 'Failed to delete achievement', 'error');
|
|
}
|
|
} catch (e) {
|
|
showSaveStatus('achievementSaveStatus', 'Failed to delete achievement', 'error');
|
|
}
|
|
}
|
|
|
|
async function loadAdminShop() {
|
|
if (!currentGuild) return;
|
|
|
|
const container = document.getElementById('adminShopList');
|
|
container.innerHTML = '<div style="display:flex;justify-content:center;padding:3rem"><div class="loading-spinner"></div></div>';
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/shop');
|
|
if (!res.ok) {
|
|
container.innerHTML = '<div class="empty-state"><p>Failed to load shop items</p></div>';
|
|
return;
|
|
}
|
|
|
|
const data = await res.json();
|
|
|
|
if (!data.items || data.items.length === 0) {
|
|
container.innerHTML = '<div class="empty-state"><div class="empty-state-icon">🏪</div><p>No shop items configured yet</p></div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = data.items.map(item => `
|
|
<div class="item-row">
|
|
<div style="font-size:1.5rem">${ITEM_TYPES[item.item_type]?.emoji || '🎁'}</div>
|
|
<div class="item-info">
|
|
<div class="item-name">
|
|
${item.name}
|
|
<span class="item-badge ${item.enabled ? 'active' : 'inactive'}">${item.enabled ? 'Active' : 'Disabled'}</span>
|
|
</div>
|
|
<div class="item-desc">${item.price.toLocaleString()} XP | ${ITEM_TYPES[item.item_type]?.name || item.item_type}</div>
|
|
</div>
|
|
<div class="item-actions">
|
|
<button class="btn-icon" onclick="editShopItem(${item.id})" title="Edit">✏️</button>
|
|
<button class="btn-icon danger" onclick="deleteShopItem(${item.id})" title="Delete">🗑️</button>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
window.adminShopItems = data.items;
|
|
|
|
} catch (e) {
|
|
container.innerHTML = '<div class="empty-state"><p>Failed to load shop items</p></div>';
|
|
}
|
|
}
|
|
|
|
function openShopModal(item = null) {
|
|
document.getElementById('shopModalTitle').textContent = item ? 'Edit Shop Item' : 'Add Shop Item';
|
|
document.getElementById('shopEditId').value = item?.id || '';
|
|
document.getElementById('shopItemName').value = item?.name || '';
|
|
document.getElementById('shopItemType').value = item?.item_type || 'badge';
|
|
document.getElementById('shopItemDescription').value = item?.description || '';
|
|
document.getElementById('shopItemPrice').value = item?.price || 100;
|
|
document.getElementById('shopItemStock').value = item?.stock ?? -1;
|
|
document.getElementById('shopItemLevelReq').value = item?.level_required || 0;
|
|
document.getElementById('shopItemPrestigeReq').value = item?.prestige_required || 0;
|
|
document.getElementById('shopItemEnabled').checked = item?.enabled !== false;
|
|
document.getElementById('shopModal').classList.remove('hidden');
|
|
}
|
|
|
|
function closeShopModal() {
|
|
document.getElementById('shopModal').classList.add('hidden');
|
|
}
|
|
|
|
function editShopItem(itemId) {
|
|
const item = window.adminShopItems?.find(i => i.id === itemId);
|
|
if (item) openShopModal(item);
|
|
}
|
|
|
|
async function saveShopItem(e) {
|
|
e.preventDefault();
|
|
|
|
if (!currentGuild) return;
|
|
|
|
const itemId = document.getElementById('shopEditId').value;
|
|
const data = {
|
|
name: document.getElementById('shopItemName').value,
|
|
item_type: document.getElementById('shopItemType').value,
|
|
description: document.getElementById('shopItemDescription').value,
|
|
price: parseInt(document.getElementById('shopItemPrice').value),
|
|
stock: parseInt(document.getElementById('shopItemStock').value) || -1,
|
|
level_required: parseInt(document.getElementById('shopItemLevelReq').value) || 0,
|
|
prestige_required: parseInt(document.getElementById('shopItemPrestigeReq').value) || 0,
|
|
enabled: document.getElementById('shopItemEnabled').checked
|
|
};
|
|
|
|
try {
|
|
const url = itemId
|
|
? '/api/guild/' + currentGuild + '/shop/' + itemId
|
|
: '/api/guild/' + currentGuild + '/shop';
|
|
|
|
const res = await fetch(url, {
|
|
method: itemId ? 'PUT' : 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
|
|
if (res.ok) {
|
|
closeShopModal();
|
|
showSaveStatus('shopSaveStatus', itemId ? 'Item updated!' : 'Item created!', 'success');
|
|
await loadAdminShop();
|
|
} else {
|
|
const err = await res.json();
|
|
showSaveStatus('shopSaveStatus', err.error || 'Failed to save item', 'error');
|
|
}
|
|
} catch (e) {
|
|
showSaveStatus('shopSaveStatus', 'Failed to save item', 'error');
|
|
}
|
|
}
|
|
|
|
async function deleteShopItem(itemId) {
|
|
if (!confirm('Are you sure you want to delete this item?')) return;
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/shop/' + itemId, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
if (res.ok) {
|
|
showSaveStatus('shopSaveStatus', 'Item deleted', 'success');
|
|
await loadAdminShop();
|
|
} else {
|
|
showSaveStatus('shopSaveStatus', 'Failed to delete item', 'error');
|
|
}
|
|
} catch (e) {
|
|
showSaveStatus('shopSaveStatus', 'Failed to delete item', 'error');
|
|
}
|
|
}
|
|
|
|
document.querySelectorAll('[data-fed-tab]').forEach(tab => {
|
|
tab.addEventListener('click', () => {
|
|
document.querySelectorAll('[data-fed-tab]').forEach(t => t.classList.remove('active'));
|
|
document.querySelectorAll('.fed-section').forEach(s => s.classList.add('hidden'));
|
|
tab.classList.add('active');
|
|
document.getElementById(tab.dataset.fedTab).classList.remove('hidden');
|
|
});
|
|
});
|
|
|
|
async function loadFederationStats() {
|
|
try {
|
|
const res = await fetch('/api/federation/stats');
|
|
const data = await res.json();
|
|
document.getElementById('fedTotalServers').textContent = data.totalServers || 0;
|
|
document.getElementById('fedActiveBans').textContent = data.activeBans || 0;
|
|
document.getElementById('fedPendingApps').textContent = data.pendingApplications || 0;
|
|
} catch (e) {
|
|
console.error('Failed to load federation stats:', e);
|
|
}
|
|
}
|
|
|
|
async function loadFederationServers() {
|
|
try {
|
|
const res = await fetch('/api/federation/servers');
|
|
const data = await res.json();
|
|
const grid = document.getElementById('fedServerGrid');
|
|
|
|
if (!data.servers || data.servers.length === 0) {
|
|
grid.innerHTML = '<div class="empty-state">No servers in the federation yet. Use /federation apply to join!</div>';
|
|
return;
|
|
}
|
|
|
|
const categoryEmojis = { gaming: '🎮', creative: '🎨', development: '💻', education: '📚', community: '👥', business: '🏢' };
|
|
|
|
grid.innerHTML = data.servers.map(s => `
|
|
<div class="card" style="padding:1rem">
|
|
<div style="display:flex;align-items:center;gap:0.75rem;margin-bottom:0.5rem">
|
|
<div style="width:40px;height:40px;background:var(--primary);border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:1.25rem">${categoryEmojis[s.category] || '🌐'}</div>
|
|
<div>
|
|
<div style="font-weight:600">${s.guild_name}</div>
|
|
<div style="font-size:0.8rem;color:var(--muted)">${s.category || 'General'}</div>
|
|
</div>
|
|
</div>
|
|
<div style="font-size:0.85rem;color:var(--muted);margin-bottom:0.5rem">${s.description || 'No description'}</div>
|
|
<div style="font-size:0.8rem;color:var(--muted)">${(s.member_count || 0).toLocaleString()} members</div>
|
|
</div>
|
|
`).join('');
|
|
} catch (e) {
|
|
console.error('Failed to load federation servers:', e);
|
|
}
|
|
}
|
|
|
|
async function loadFederationBans() {
|
|
try {
|
|
const res = await fetch('/api/federation/bans?limit=50');
|
|
const data = await res.json();
|
|
const tbody = document.getElementById('fedBanList');
|
|
|
|
if (!data.bans || data.bans.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="4" class="empty-state">No active bans</td></tr>';
|
|
return;
|
|
}
|
|
|
|
const severityColors = { low: 'var(--muted)', medium: 'var(--warning)', high: '#f97316', critical: 'var(--danger)' };
|
|
|
|
tbody.innerHTML = data.bans.map(b => `
|
|
<tr>
|
|
<td>${b.username || b.user_id}</td>
|
|
<td><span style="padding:0.25rem 0.5rem;border-radius:4px;font-size:0.75rem;background:${severityColors[b.severity] || 'var(--muted)'};color:white">${(b.severity || 'unknown').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 federation bans:', e);
|
|
}
|
|
}
|
|
|
|
async function loadFederationApplications() {
|
|
try {
|
|
const res = await fetch('/api/federation/applications');
|
|
const data = await res.json();
|
|
const tbody = document.getElementById('fedAppList');
|
|
|
|
if (!data.applications || data.applications.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="5" class="empty-state">No applications</td></tr>';
|
|
return;
|
|
}
|
|
|
|
const statusColors = { pending: 'var(--warning)', approved: 'var(--success)', rejected: 'var(--danger)' };
|
|
|
|
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 style="padding:0.25rem 0.5rem;border-radius:4px;font-size:0.75rem;background:${statusColors[a.status] || 'var(--muted)'};color:white">${(a.status || 'pending').toUpperCase()}</span></td>
|
|
<td>
|
|
${a.status === 'pending' ? `
|
|
<button class="btn btn-primary" style="padding:0.25rem 0.5rem;font-size:0.75rem" onclick="approveFedApp(${a.id})">Approve</button>
|
|
<button class="btn btn-secondary" style="padding:0.25rem 0.5rem;font-size:0.75rem" onclick="rejectFedApp(${a.id})">Reject</button>
|
|
` : '-'}
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
} catch (e) {
|
|
console.error('Failed to load federation applications:', e);
|
|
}
|
|
}
|
|
|
|
async function loadFederationLeaderboard() {
|
|
try {
|
|
const res = await fetch('/api/federation/leaderboard?limit=20');
|
|
const data = await res.json();
|
|
const container = document.getElementById('fedLeaderboardList');
|
|
|
|
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) => `
|
|
<div style="display:flex;align-items:center;gap:1rem;padding:0.75rem;border-bottom:1px solid var(--border)">
|
|
<div style="width:32px;height:32px;border-radius:50%;background:${i < 3 ? 'linear-gradient(135deg,var(--primary),#3b82f6)' : 'var(--secondary)'};display:flex;align-items:center;justify-content:center;font-weight:700;font-size:0.875rem">${i + 1}</div>
|
|
<div style="flex:1">
|
|
<div style="font-weight:500">${l.discord_id}</div>
|
|
<div style="font-size:0.8rem;color:var(--muted)">${tierEmojis[l.rank_tier] || '🌱'} ${(l.rank_tier || 'newcomer').toUpperCase()}</div>
|
|
</div>
|
|
<div style="font-weight:600;color:var(--primary)">${(l.reputation_score || 0).toLocaleString()} rep</div>
|
|
</div>
|
|
`).join('');
|
|
} catch (e) {
|
|
console.error('Failed to load federation leaderboard:', e);
|
|
}
|
|
}
|
|
|
|
async function approveFedApp(id) {
|
|
if (!confirm('Approve this application?')) return;
|
|
try {
|
|
await fetch('/api/federation/applications/' + id + '/approve', { method: 'POST' });
|
|
loadFederationApplications();
|
|
loadFederationStats();
|
|
loadFederationServers();
|
|
} catch (e) {
|
|
alert('Failed to approve');
|
|
}
|
|
}
|
|
|
|
async function rejectFedApp(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 })
|
|
});
|
|
loadFederationApplications();
|
|
loadFederationStats();
|
|
} catch (e) {
|
|
alert('Failed to reject');
|
|
}
|
|
}
|
|
|
|
function loadFederationData() {
|
|
loadFederationStats();
|
|
loadFederationServers();
|
|
loadFederationBans();
|
|
loadFederationApplications();
|
|
loadFederationLeaderboard();
|
|
}
|
|
|
|
// Moderation Tab Event Listeners
|
|
document.querySelectorAll('[data-mod-tab]').forEach(tab => {
|
|
tab.addEventListener('click', () => {
|
|
document.querySelectorAll('[data-mod-tab]').forEach(t => t.classList.remove('active'));
|
|
document.querySelectorAll('.mod-section').forEach(s => s.classList.add('hidden'));
|
|
tab.classList.add('active');
|
|
document.getElementById(tab.dataset.modTab).classList.remove('hidden');
|
|
});
|
|
});
|
|
|
|
function loadModerationData() {
|
|
loadModerationStats();
|
|
loadWarnings();
|
|
loadBans();
|
|
loadActivityFeed();
|
|
}
|
|
|
|
async function loadModerationStats() {
|
|
if (!currentGuild) return;
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/moderation/stats');
|
|
const data = await res.json();
|
|
document.getElementById('modTotalWarnings').textContent = data.totalWarnings || 0;
|
|
document.getElementById('modActiveBans').textContent = data.activeBans || 0;
|
|
document.getElementById('modRecentActions').textContent = data.recentActions || 0;
|
|
} catch (e) {
|
|
console.error('Failed to load moderation stats:', e);
|
|
}
|
|
}
|
|
|
|
async function loadWarnings() {
|
|
if (!currentGuild) return;
|
|
const tbody = document.getElementById('modWarningsList');
|
|
tbody.innerHTML = '<tr><td colspan="5" class="empty-state">Loading warnings...</td></tr>';
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/moderation/warnings');
|
|
const data = await res.json();
|
|
|
|
if (!data.warnings || data.warnings.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="5" class="empty-state">No warnings found</td></tr>';
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = data.warnings.map(w => `
|
|
<tr>
|
|
<td>${escapeHtml(w.username || w.user_id)}</td>
|
|
<td>${escapeHtml((w.reason || 'No reason').substring(0, 50))}${(w.reason || '').length > 50 ? '...' : ''}</td>
|
|
<td>${escapeHtml(w.moderator_name || w.moderator_id || 'Unknown')}</td>
|
|
<td>${new Date(w.created_at).toLocaleDateString()}</td>
|
|
<td><button class="btn-icon danger" onclick="deleteWarning('${w.id}')" title="Delete Warning">✕</button></td>
|
|
</tr>
|
|
`).join('');
|
|
} catch (e) {
|
|
tbody.innerHTML = '<tr><td colspan="5" class="empty-state">Failed to load warnings</td></tr>';
|
|
}
|
|
}
|
|
|
|
async function loadBans() {
|
|
if (!currentGuild) return;
|
|
const tbody = document.getElementById('modBansList');
|
|
tbody.innerHTML = '<tr><td colspan="4" class="empty-state">Loading bans...</td></tr>';
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/moderation/bans');
|
|
const data = await res.json();
|
|
|
|
if (!data.bans || data.bans.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="4" class="empty-state">No bans found</td></tr>';
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = data.bans.map(b => `
|
|
<tr>
|
|
<td>${escapeHtml(b.username || b.user_id)}</td>
|
|
<td>${escapeHtml((b.reason || 'No reason').substring(0, 50))}${(b.reason || '').length > 50 ? '...' : ''}</td>
|
|
<td>${escapeHtml(b.moderator_name || b.moderator_id || 'Unknown')}</td>
|
|
<td>${new Date(b.created_at).toLocaleDateString()}</td>
|
|
</tr>
|
|
`).join('');
|
|
} catch (e) {
|
|
tbody.innerHTML = '<tr><td colspan="4" class="empty-state">Failed to load bans</td></tr>';
|
|
}
|
|
}
|
|
|
|
async function loadActivityFeed() {
|
|
if (!currentGuild) return;
|
|
const container = document.getElementById('modActivityFeed');
|
|
container.innerHTML = '<div class="empty-state">Loading activity...</div>';
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/moderation/activity');
|
|
const data = await res.json();
|
|
|
|
if (!data.activity || data.activity.length === 0) {
|
|
container.innerHTML = '<div class="empty-state">No recent moderation activity</div>';
|
|
return;
|
|
}
|
|
|
|
const actionColors = { warn: 'var(--warning)', kick: '#f97316', ban: 'var(--danger)', timeout: 'var(--primary)', unban: 'var(--success)' };
|
|
|
|
container.innerHTML = data.activity.map(a => `
|
|
<div style="display:flex;align-items:center;gap:1rem;padding:0.75rem;border-bottom:1px solid var(--border)">
|
|
<div style="width:8px;height:8px;border-radius:50%;background:${actionColors[a.action_type] || 'var(--muted)'}"></div>
|
|
<div style="flex:1">
|
|
<div style="font-weight:500">${escapeHtml(a.target_name || a.target_id)} was ${a.action_type}${a.action_type.endsWith('n') ? 'ned' : 'ed'}</div>
|
|
<div style="font-size:0.8rem;color:var(--muted)">by ${escapeHtml(a.moderator_name || a.moderator_id)} - ${a.reason ? escapeHtml(a.reason.substring(0, 40)) : 'No reason'}</div>
|
|
</div>
|
|
<div style="font-size:0.75rem;color:var(--muted)">${new Date(a.created_at).toLocaleString()}</div>
|
|
</div>
|
|
`).join('');
|
|
} catch (e) {
|
|
container.innerHTML = '<div class="empty-state">Failed to load activity</div>';
|
|
}
|
|
}
|
|
|
|
async function searchModUser() {
|
|
if (!currentGuild) return;
|
|
const query = document.getElementById('modSearchInput').value.trim();
|
|
if (!query) return;
|
|
|
|
const container = document.getElementById('modSearchResults');
|
|
container.innerHTML = '<div style="display:flex;justify-content:center;padding:1rem"><div class="loading-spinner"></div></div>';
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/moderation/search?q=' + encodeURIComponent(query));
|
|
const data = await res.json();
|
|
|
|
if (!data.results || data.results.length === 0) {
|
|
container.innerHTML = '<div style="padding:1rem;color:var(--muted)">No users found matching "' + escapeHtml(query) + '"</div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = data.results.map(u => `
|
|
<div style="display:flex;align-items:center;gap:1rem;padding:0.75rem;background:var(--card);border:1px solid var(--card-border);border-radius:8px;margin-bottom:0.5rem">
|
|
<div style="width:40px;height:40px;border-radius:50%;background:linear-gradient(135deg,var(--primary),#3b82f6);display:flex;align-items:center;justify-content:center;font-weight:600">${(u.username || 'U').charAt(0).toUpperCase()}</div>
|
|
<div style="flex:1">
|
|
<div style="font-weight:500">${escapeHtml(u.username || u.user_id)}</div>
|
|
<div style="font-size:0.8rem;color:var(--muted)">${u.warnings_count || 0} warnings</div>
|
|
</div>
|
|
<div style="font-size:0.8rem;color:var(--muted)">${u.is_banned ? '<span style="color:var(--danger)">BANNED</span>' : ''}</div>
|
|
</div>
|
|
`).join('');
|
|
} catch (e) {
|
|
container.innerHTML = '<div style="padding:1rem;color:var(--danger)">Search failed</div>';
|
|
}
|
|
}
|
|
|
|
async function deleteWarning(warningId) {
|
|
if (!confirm('Delete this warning?')) return;
|
|
if (!currentGuild) return;
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/moderation/warnings/' + warningId, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
if (res.ok) {
|
|
loadWarnings();
|
|
loadModerationStats();
|
|
} else {
|
|
alert('Failed to delete warning');
|
|
}
|
|
} catch (e) {
|
|
alert('Failed to delete warning');
|
|
}
|
|
}
|
|
|
|
function updateBulkActionForm() {
|
|
const action = document.getElementById('bulkAction').value;
|
|
const btn = document.getElementById('bulkActionBtn');
|
|
const timeoutDiv = document.getElementById('bulkTimeoutDuration');
|
|
const deleteDaysDiv = document.getElementById('bulkDeleteDays');
|
|
const reasonGroup = document.getElementById('bulkReasonGroup');
|
|
|
|
const actionNames = {
|
|
ban: 'Execute Bulk Ban',
|
|
kick: 'Execute Bulk Kick',
|
|
timeout: 'Execute Bulk Timeout',
|
|
warn: 'Execute Bulk Warning',
|
|
remove_timeout: 'Remove Timeouts'
|
|
};
|
|
|
|
btn.textContent = actionNames[action] || 'Execute Action';
|
|
btn.className = action === 'remove_timeout' ? 'btn btn-primary' : 'btn btn-danger';
|
|
|
|
timeoutDiv.style.display = action === 'timeout' ? 'block' : 'none';
|
|
deleteDaysDiv.style.display = action === 'ban' ? 'block' : 'none';
|
|
reasonGroup.style.display = action === 'remove_timeout' ? 'none' : 'block';
|
|
}
|
|
|
|
function parseUserIdsFromInput(input) {
|
|
const ids = input.match(/\d{17,20}/g) || [];
|
|
return [...new Set(ids)];
|
|
}
|
|
|
|
async function executeBulkAction() {
|
|
if (!currentGuild) return;
|
|
|
|
const action = document.getElementById('bulkAction').value;
|
|
const userIdsInput = document.getElementById('bulkUserIds').value;
|
|
const reason = document.getElementById('bulkReason').value || 'No reason provided';
|
|
const duration = document.getElementById('bulkDuration').value;
|
|
const deleteDays = document.getElementById('bulkBanDeleteDays').value;
|
|
|
|
const userIds = parseUserIdsFromInput(userIdsInput);
|
|
|
|
if (userIds.length === 0) {
|
|
alert('Please enter at least one valid user ID');
|
|
return;
|
|
}
|
|
|
|
if (userIds.length > 25) {
|
|
alert('Maximum 25 users per bulk action');
|
|
return;
|
|
}
|
|
|
|
const actionNames = { ban: 'ban', kick: 'kick', timeout: 'timeout', warn: 'warn', remove_timeout: 'remove timeout from' };
|
|
if (!confirm(`Are you sure you want to ${actionNames[action]} ${userIds.length} user(s)? This action cannot be undone.`)) {
|
|
return;
|
|
}
|
|
|
|
const btn = document.getElementById('bulkActionBtn');
|
|
const status = document.getElementById('bulkActionStatus');
|
|
btn.disabled = true;
|
|
status.textContent = 'Processing...';
|
|
status.style.color = 'var(--text-secondary)';
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/moderation/bulk', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
action,
|
|
userIds,
|
|
reason,
|
|
duration,
|
|
deleteDays: parseInt(deleteDays, 10)
|
|
})
|
|
});
|
|
|
|
const data = await res.json();
|
|
|
|
if (!res.ok) {
|
|
throw new Error(data.error || 'Bulk action failed');
|
|
}
|
|
|
|
status.textContent = 'Complete!';
|
|
status.style.color = 'var(--success)';
|
|
|
|
displayBulkResults(action, data);
|
|
|
|
document.getElementById('bulkUserIds').value = '';
|
|
document.getElementById('bulkReason').value = '';
|
|
|
|
loadModerationStats();
|
|
loadActivityFeed();
|
|
} catch (e) {
|
|
status.textContent = 'Error: ' + e.message;
|
|
status.style.color = 'var(--danger)';
|
|
} finally {
|
|
btn.disabled = false;
|
|
}
|
|
}
|
|
|
|
function displayBulkResults(action, data) {
|
|
const container = document.getElementById('bulkActionResults');
|
|
const actionNames = { ban: 'Banned', kick: 'Kicked', timeout: 'Timed out', warn: 'Warned', remove_timeout: 'Timeout removed' };
|
|
|
|
let html = '';
|
|
|
|
if (data.success && data.success.length > 0) {
|
|
html += '<div style="margin-bottom:1rem">';
|
|
html += '<div style="color:var(--success);font-weight:600;margin-bottom:0.5rem">Successful (' + data.success.length + ')</div>';
|
|
html += '<div style="display:flex;flex-wrap:wrap;gap:0.5rem">';
|
|
data.success.forEach(id => {
|
|
html += '<span style="background:rgba(34,197,94,0.2);color:var(--success);padding:0.25rem 0.5rem;border-radius:4px;font-size:0.85rem">' + id + '</span>';
|
|
});
|
|
html += '</div></div>';
|
|
}
|
|
|
|
if (data.failed && data.failed.length > 0) {
|
|
html += '<div>';
|
|
html += '<div style="color:var(--danger);font-weight:600;margin-bottom:0.5rem">Failed (' + data.failed.length + ')</div>';
|
|
html += '<div style="display:flex;flex-direction:column;gap:0.25rem">';
|
|
data.failed.forEach(f => {
|
|
html += '<div style="font-size:0.85rem;color:var(--text-secondary)">' + f.id + ': <span style="color:var(--danger)">' + escapeHtml(f.reason) + '</span></div>';
|
|
});
|
|
html += '</div></div>';
|
|
}
|
|
|
|
if (!html) {
|
|
html = '<div class="empty-state">No users processed</div>';
|
|
}
|
|
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
// Analytics Functions
|
|
function loadAnalyticsData() {
|
|
loadAnalyticsStats();
|
|
loadActivityChart();
|
|
loadTopEarners();
|
|
loadTopChannels();
|
|
loadCommandStats();
|
|
}
|
|
|
|
async function loadAnalyticsStats() {
|
|
if (!currentGuild) return;
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/analytics/stats');
|
|
const data = await res.json();
|
|
document.getElementById('analyticsMessagesToday').textContent = (data.messagesToday || 0).toLocaleString();
|
|
document.getElementById('analyticsActiveUsers').textContent = (data.activeUsers || 0).toLocaleString();
|
|
document.getElementById('analyticsXpToday').textContent = (data.xpToday || 0).toLocaleString();
|
|
document.getElementById('analyticsCommandsUsed').textContent = (data.commandsUsed || 0).toLocaleString();
|
|
} catch (e) {
|
|
console.error('Failed to load analytics stats:', e);
|
|
}
|
|
}
|
|
|
|
async function loadActivityChart() {
|
|
if (!currentGuild) return;
|
|
const container = document.getElementById('activityChart');
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/analytics/activity');
|
|
const data = await res.json();
|
|
|
|
if (!data.days || data.days.length === 0) {
|
|
container.innerHTML = '<div class="empty-state" style="width:100%">No activity data available</div>';
|
|
return;
|
|
}
|
|
|
|
const maxVal = Math.max(...data.days.map(d => d.messages || 0), 1);
|
|
|
|
container.innerHTML = data.days.map(d => {
|
|
const height = Math.max(10, ((d.messages || 0) / maxVal) * 150);
|
|
return `
|
|
<div style="flex:1;display:flex;flex-direction:column;align-items:center;gap:0.5rem">
|
|
<div style="width:100%;max-width:40px;height:${height}px;background:linear-gradient(180deg,var(--primary),#3b82f6);border-radius:4px 4px 0 0;transition:height 0.3s"></div>
|
|
<div style="font-size:0.7rem;color:var(--muted)">${d.label || ''}</div>
|
|
<div style="font-size:0.65rem;color:var(--muted)">${(d.messages || 0).toLocaleString()}</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
} catch (e) {
|
|
container.innerHTML = '<div class="empty-state" style="width:100%">Failed to load chart</div>';
|
|
}
|
|
}
|
|
|
|
async function loadTopEarners() {
|
|
if (!currentGuild) return;
|
|
const container = document.getElementById('analyticsTopEarners');
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/analytics/top-earners');
|
|
const data = await res.json();
|
|
|
|
if (!data.earners || data.earners.length === 0) {
|
|
container.innerHTML = '<div class="empty-state">No XP earned today</div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = data.earners.map((e, i) => `
|
|
<div style="display:flex;align-items:center;gap:0.75rem;padding:0.5rem 0;${i < data.earners.length - 1 ? 'border-bottom:1px solid var(--border)' : ''}">
|
|
<div style="width:24px;font-weight:600;color:${i < 3 ? 'var(--primary)' : 'var(--muted)'}">#${i + 1}</div>
|
|
<div style="flex:1">${escapeHtml(e.username || e.user_id)}</div>
|
|
<div style="font-weight:600;color:var(--primary)">${(e.xp_earned || 0).toLocaleString()} XP</div>
|
|
</div>
|
|
`).join('');
|
|
} catch (e) {
|
|
container.innerHTML = '<div class="empty-state">Failed to load</div>';
|
|
}
|
|
}
|
|
|
|
async function loadTopChannels() {
|
|
if (!currentGuild) return;
|
|
const container = document.getElementById('analyticsTopChannels');
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/analytics/top-channels');
|
|
const data = await res.json();
|
|
|
|
if (!data.channels || data.channels.length === 0) {
|
|
container.innerHTML = '<div class="empty-state">No channel data</div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = data.channels.map((c, i) => `
|
|
<div style="display:flex;align-items:center;gap:0.75rem;padding:0.5rem 0;${i < data.channels.length - 1 ? 'border-bottom:1px solid var(--border)' : ''}">
|
|
<div style="color:var(--muted)">#</div>
|
|
<div style="flex:1">${escapeHtml(c.channel_name || c.channel_id)}</div>
|
|
<div style="font-size:0.85rem;color:var(--muted)">${(c.message_count || 0).toLocaleString()} msgs</div>
|
|
</div>
|
|
`).join('');
|
|
} catch (e) {
|
|
container.innerHTML = '<div class="empty-state">Failed to load</div>';
|
|
}
|
|
}
|
|
|
|
async function loadCommandStats() {
|
|
if (!currentGuild) return;
|
|
const container = document.getElementById('analyticsCommandStats');
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/analytics/commands');
|
|
const data = await res.json();
|
|
|
|
if (!data.commands || data.commands.length === 0) {
|
|
container.innerHTML = '<div class="empty-state">No command data</div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = data.commands.map((c, i) => `
|
|
<div style="display:flex;align-items:center;gap:0.75rem;padding:0.5rem 0;${i < data.commands.length - 1 ? 'border-bottom:1px solid var(--border)' : ''}">
|
|
<div style="color:var(--primary)">/${escapeHtml(c.command_name)}</div>
|
|
<div style="flex:1"></div>
|
|
<div style="font-size:0.85rem;color:var(--muted)">${(c.use_count || 0).toLocaleString()} uses</div>
|
|
</div>
|
|
`).join('');
|
|
} catch (e) {
|
|
container.innerHTML = '<div class="empty-state">Failed to load</div>';
|
|
}
|
|
}
|
|
|
|
// Titles Functions
|
|
async function loadTitles() {
|
|
if (!currentGuild) return;
|
|
const container = document.getElementById('titlesList');
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/titles');
|
|
const data = await res.json();
|
|
|
|
// Update active title display
|
|
if (data.activeTitle) {
|
|
document.getElementById('activeTitleName').textContent = data.activeTitle.name;
|
|
document.getElementById('activeTitleDesc').textContent = data.activeTitle.description || 'Your current display title';
|
|
document.getElementById('clearTitleBtn').style.display = 'block';
|
|
} else {
|
|
document.getElementById('activeTitleName').textContent = 'No title selected';
|
|
document.getElementById('activeTitleDesc').textContent = 'Select a title from your collection below';
|
|
document.getElementById('clearTitleBtn').style.display = 'none';
|
|
}
|
|
|
|
if (!data.titles || data.titles.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="empty-state">
|
|
<div class="empty-state-icon">🏷️</div>
|
|
<p>You don't have any titles yet</p>
|
|
<p style="font-size:0.875rem;color:var(--muted);margin-top:0.5rem">Purchase titles from the shop or earn them through achievements</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = data.titles.map(t => `
|
|
<div class="item-row">
|
|
<div style="width:40px;height:40px;background:linear-gradient(135deg,var(--primary),#3b82f6);border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:1.25rem">🏷️</div>
|
|
<div class="item-info">
|
|
<div class="item-name">
|
|
${escapeHtml(t.name)}
|
|
${t.is_active ? '<span class="item-badge active">Active</span>' : ''}
|
|
</div>
|
|
<div class="item-desc">${escapeHtml(t.description || 'Custom title')}</div>
|
|
</div>
|
|
<div class="item-actions">
|
|
${!t.is_active ? `<button class="btn btn-primary" style="padding:0.5rem 1rem" onclick="setActiveTitle('${t.id}')">Use</button>` : ''}
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
} catch (e) {
|
|
container.innerHTML = '<div class="empty-state">Failed to load titles</div>';
|
|
}
|
|
}
|
|
|
|
async function setActiveTitle(titleId) {
|
|
if (!currentGuild) return;
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/titles/active', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ title_id: titleId })
|
|
});
|
|
|
|
if (res.ok) {
|
|
await loadTitles();
|
|
} else {
|
|
alert('Failed to set title');
|
|
}
|
|
} catch (e) {
|
|
alert('Failed to set title');
|
|
}
|
|
}
|
|
|
|
async function clearActiveTitle() {
|
|
if (!currentGuild) return;
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/titles/active', {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
if (res.ok) {
|
|
await loadTitles();
|
|
} else {
|
|
alert('Failed to clear title');
|
|
}
|
|
} catch (e) {
|
|
alert('Failed to clear title');
|
|
}
|
|
}
|
|
|
|
// Coins functions
|
|
async function loadCoins() {
|
|
if (!currentGuild) return;
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/coins');
|
|
if (!res.ok) throw new Error('Failed to load');
|
|
const data = await res.json();
|
|
|
|
const coinName = data.coinName || 'Coins';
|
|
|
|
document.getElementById('coinPageTitle').textContent = coinName;
|
|
document.getElementById('coinBalance').textContent = (data.coins || 0).toLocaleString();
|
|
document.getElementById('coinCurrencyName').textContent = coinName;
|
|
document.getElementById('statCoins').textContent = (data.coins || 0).toLocaleString();
|
|
document.getElementById('statCoinLabel').textContent = coinName;
|
|
|
|
// Load leaderboard
|
|
loadCoinLeaderboard(coinName);
|
|
|
|
// Show admin card if user is admin
|
|
const adminCard = document.getElementById('coinAdminCard');
|
|
if (isCurrentGuildAdmin()) {
|
|
adminCard.style.display = 'block';
|
|
loadCoinConfig();
|
|
} else {
|
|
adminCard.style.display = 'none';
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to load coins:', e);
|
|
}
|
|
}
|
|
|
|
async function loadCoinLeaderboard(coinName) {
|
|
if (!currentGuild) return;
|
|
|
|
const container = document.getElementById('coinLeaderboardBody');
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/coins/leaderboard?limit=10');
|
|
if (!res.ok) throw new Error('Failed to load');
|
|
const data = await res.json();
|
|
|
|
if (!data.leaderboard || data.leaderboard.length === 0) {
|
|
container.innerHTML = '<div class="empty-state"><div class="empty-state-icon">🪙</div><p>No coin leaderboard data yet</p></div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = data.leaderboard.map((entry, idx) => `
|
|
<div class="leaderboard-item ${idx < 3 ? 'top-3' : ''}">
|
|
<div class="leaderboard-rank">${idx + 1}</div>
|
|
<div class="leaderboard-avatar" style="background:linear-gradient(135deg, var(--gradient-1), var(--gradient-2));display:flex;align-items:center;justify-content:center;color:#fff;font-weight:600">${(entry.user_id || '?').substring(0, 2).toUpperCase()}</div>
|
|
<span class="leaderboard-name">User ${entry.user_id?.substring(0, 8) || 'Unknown'}</span>
|
|
<span class="leaderboard-xp">${(entry.coins || 0).toLocaleString()} ${coinName || 'Coins'}</span>
|
|
</div>
|
|
`).join('');
|
|
} catch (e) {
|
|
container.innerHTML = '<div class="empty-state">Failed to load leaderboard</div>';
|
|
}
|
|
}
|
|
|
|
async function loadCoinConfig() {
|
|
if (!currentGuild) return;
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/coins/config');
|
|
if (!res.ok) return;
|
|
const data = await res.json();
|
|
const config = data.config || {};
|
|
|
|
document.getElementById('coinName').value = config.coin_name || 'Coins';
|
|
document.getElementById('messageCoins').value = config.message_coins ?? 1;
|
|
document.getElementById('dailyCoins').value = config.daily_coins ?? 50;
|
|
document.getElementById('coinsEnabled').checked = config.coins_enabled !== false;
|
|
} catch (e) {
|
|
console.error('Failed to load coin config:', e);
|
|
}
|
|
}
|
|
|
|
async function saveCoinConfig(e) {
|
|
e.preventDefault();
|
|
if (!currentGuild) return;
|
|
|
|
const formData = {
|
|
coin_name: document.getElementById('coinName').value || 'Coins',
|
|
message_coins: parseInt(document.getElementById('messageCoins').value) || 1,
|
|
daily_coins: parseInt(document.getElementById('dailyCoins').value) || 50,
|
|
coins_enabled: document.getElementById('coinsEnabled').checked
|
|
};
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/coins/config', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(formData)
|
|
});
|
|
|
|
if (res.ok) {
|
|
alert('Coin settings saved!');
|
|
loadCoins();
|
|
} else {
|
|
alert('Failed to save coin settings');
|
|
}
|
|
} catch (e) {
|
|
alert('Failed to save coin settings');
|
|
}
|
|
}
|
|
|
|
function isCurrentGuildAdmin() {
|
|
if (!currentUser || !currentGuild) return false;
|
|
const guild = currentUser.guilds?.find(g => g.id === currentGuild);
|
|
return guild?.isAdmin === true;
|
|
}
|
|
|
|
// Activity Roles Functions
|
|
async function loadActivityRoles() {
|
|
if (!currentGuild) return;
|
|
|
|
await loadGuildRoles();
|
|
|
|
const container = document.getElementById('activityRolesList');
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/activity-roles');
|
|
if (!res.ok) throw new Error('Failed to load');
|
|
const data = await res.json();
|
|
|
|
if (!data.roles || data.roles.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="empty-state">
|
|
<div class="empty-state-icon">🎯</div>
|
|
<p>No activity roles configured</p>
|
|
<p style="font-size:0.875rem;color:var(--muted);margin-top:0.5rem">Add roles above to automatically reward active members</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
const typeLabels = {
|
|
messages: 'Messages',
|
|
voice_hours: 'Voice Hours',
|
|
daily_streak: 'Daily Streak',
|
|
reactions_given: 'Reactions Given',
|
|
reactions_received: 'Reactions Received',
|
|
commands_used: 'Commands Used'
|
|
};
|
|
|
|
const typeSuffix = {
|
|
messages: ' messages',
|
|
voice_hours: ' hours',
|
|
daily_streak: ' days',
|
|
reactions_given: ' reactions',
|
|
reactions_received: ' reactions',
|
|
commands_used: ' commands'
|
|
};
|
|
|
|
container.innerHTML = data.roles.map(r => `
|
|
<div class="item-row">
|
|
<div style="width:40px;height:40px;background:${r.role_color || '#6366f1'};border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:1.25rem">🎯</div>
|
|
<div class="item-info">
|
|
<div class="item-name">
|
|
${escapeHtml(r.role_name)}
|
|
<span class="item-badge ${r.stack_roles ? 'active' : 'inactive'}">${r.stack_roles ? 'Stacking' : 'Replace'}</span>
|
|
</div>
|
|
<div class="item-desc">${typeLabels[r.milestone_type] || r.milestone_type}: ${r.milestone_value.toLocaleString()}${typeSuffix[r.milestone_type] || ''}</div>
|
|
</div>
|
|
<div class="item-actions">
|
|
<button class="btn-icon danger" onclick="deleteActivityRole('${r.role_id}')" title="Delete">✕</button>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
} catch (e) {
|
|
container.innerHTML = '<div class="empty-state">Failed to load activity roles</div>';
|
|
}
|
|
}
|
|
|
|
async function loadGuildRoles() {
|
|
if (!currentGuild) return;
|
|
|
|
const select = document.getElementById('activityRoleSelect');
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/roles');
|
|
if (!res.ok) throw new Error('Failed to load');
|
|
const data = await res.json();
|
|
|
|
select.innerHTML = '<option value="">Select a role...</option>' +
|
|
(data.roles || []).map(r => `<option value="${r.id}" style="color:${r.color}">${escapeHtml(r.name)}</option>`).join('');
|
|
} catch (e) {
|
|
select.innerHTML = '<option value="">Failed to load roles</option>';
|
|
}
|
|
}
|
|
|
|
async function addActivityRole() {
|
|
if (!currentGuild) return;
|
|
|
|
const roleId = document.getElementById('activityRoleSelect').value;
|
|
const milestoneType = document.getElementById('activityMilestoneType').value;
|
|
const milestoneValue = parseInt(document.getElementById('activityMilestoneValue').value);
|
|
const stackRoles = document.getElementById('activityStackRoles').value === 'true';
|
|
|
|
if (!roleId) {
|
|
alert('Please select a role');
|
|
return;
|
|
}
|
|
|
|
if (!milestoneValue || milestoneValue < 1) {
|
|
alert('Please enter a valid milestone value');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/activity-roles', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
role_id: roleId,
|
|
milestone_type: milestoneType,
|
|
milestone_value: milestoneValue,
|
|
stack_roles: stackRoles
|
|
})
|
|
});
|
|
|
|
if (res.ok) {
|
|
document.getElementById('activityRoleSelect').value = '';
|
|
document.getElementById('activityMilestoneValue').value = '';
|
|
await loadActivityRoles();
|
|
} else {
|
|
const err = await res.json();
|
|
alert(err.error || 'Failed to add activity role');
|
|
}
|
|
} catch (e) {
|
|
alert('Failed to add activity role');
|
|
}
|
|
}
|
|
|
|
async function deleteActivityRole(roleId) {
|
|
if (!confirm('Remove this activity role?')) return;
|
|
if (!currentGuild) return;
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/activity-roles/' + roleId, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
if (res.ok) {
|
|
await loadActivityRoles();
|
|
} else {
|
|
alert('Failed to delete activity role');
|
|
}
|
|
} catch (e) {
|
|
alert('Failed to delete activity role');
|
|
}
|
|
}
|
|
|
|
// Command Cooldowns Functions
|
|
async function loadCooldowns() {
|
|
if (!currentGuild) return;
|
|
|
|
const container = document.getElementById('cooldownsList');
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/cooldowns');
|
|
if (!res.ok) throw new Error('Failed to load');
|
|
const data = await res.json();
|
|
|
|
if (!data.cooldowns || data.cooldowns.length === 0) {
|
|
container.innerHTML = '<div class="empty-state">No cooldown data available</div>';
|
|
return;
|
|
}
|
|
|
|
const cmdEmojis = {
|
|
work: '💼', daily: '📅', slots: '🎰', coinflip: '🪙',
|
|
rep: '⭐', trivia: '🧠', heist: '💰', duel: '⚔️',
|
|
gift: '🎁', trade: '🔄'
|
|
};
|
|
|
|
container.innerHTML = data.cooldowns.map(c => {
|
|
const currentSec = c.is_custom ? c.custom_seconds : c.default_seconds;
|
|
const displayTime = formatCooldownTime(currentSec);
|
|
const defaultTime = formatCooldownTime(c.default_seconds);
|
|
|
|
return `
|
|
<div class="item-row" style="margin-bottom:0.75rem">
|
|
<div style="width:40px;height:40px;background:linear-gradient(135deg,var(--gradient-1),var(--gradient-2));border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:1.25rem">${cmdEmojis[c.command] || '⏱️'}</div>
|
|
<div class="item-info" style="flex:1">
|
|
<div class="item-name">
|
|
/${c.command}
|
|
${c.is_custom ? '<span class="item-badge active">Custom</span>' : '<span class="item-badge">Default</span>'}
|
|
</div>
|
|
<div class="item-desc">Current: ${displayTime} ${c.is_custom ? '(Default: ' + defaultTime + ')' : ''}</div>
|
|
</div>
|
|
<div style="display:flex;align-items:center;gap:0.5rem">
|
|
<input type="number" id="cooldown-${c.command}" class="form-input" style="width:80px"
|
|
value="${currentSec}" min="0" max="604800" placeholder="seconds">
|
|
<button class="btn btn-secondary" style="padding:0.5rem 0.75rem" onclick="saveCooldown('${c.command}')">Save</button>
|
|
${c.is_custom ? `<button class="btn-icon danger" onclick="resetCooldown('${c.command}')" title="Reset to default">↺</button>` : ''}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
} catch (e) {
|
|
container.innerHTML = '<div class="empty-state">Failed to load cooldowns</div>';
|
|
}
|
|
}
|
|
|
|
function formatCooldownTime(seconds) {
|
|
if (seconds === 0) return 'No cooldown';
|
|
if (seconds < 60) return seconds + 's';
|
|
if (seconds < 3600) return Math.floor(seconds / 60) + 'm';
|
|
if (seconds < 86400) {
|
|
const h = Math.floor(seconds / 3600);
|
|
const m = Math.floor((seconds % 3600) / 60);
|
|
return m > 0 ? h + 'h ' + m + 'm' : h + 'h';
|
|
}
|
|
const d = Math.floor(seconds / 86400);
|
|
const h = Math.floor((seconds % 86400) / 3600);
|
|
return h > 0 ? d + 'd ' + h + 'h' : d + 'd';
|
|
}
|
|
|
|
async function saveCooldown(command) {
|
|
if (!currentGuild) return;
|
|
|
|
const input = document.getElementById('cooldown-' + command);
|
|
const seconds = parseInt(input.value, 10);
|
|
|
|
if (isNaN(seconds) || seconds < 0 || seconds > 604800) {
|
|
alert('Please enter a valid value between 0 and 604800 (7 days)');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/cooldowns', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ command, seconds })
|
|
});
|
|
|
|
if (res.ok) {
|
|
await loadCooldowns();
|
|
} else {
|
|
const err = await res.json();
|
|
alert(err.error || 'Failed to save cooldown');
|
|
}
|
|
} catch (e) {
|
|
alert('Failed to save cooldown');
|
|
}
|
|
}
|
|
|
|
async function resetCooldown(command) {
|
|
if (!confirm('Reset /' + command + ' to default cooldown?')) return;
|
|
if (!currentGuild) return;
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/cooldowns/' + command, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
if (res.ok) {
|
|
await loadCooldowns();
|
|
} else {
|
|
alert('Failed to reset cooldown');
|
|
}
|
|
} catch (e) {
|
|
alert('Failed to reset cooldown');
|
|
}
|
|
}
|
|
|
|
async function loadBackups() {
|
|
if (!currentGuild) return;
|
|
|
|
try {
|
|
const [backupsRes, settingsRes] = await Promise.all([
|
|
fetch('/api/guild/' + currentGuild + '/backups'),
|
|
fetch('/api/guild/' + currentGuild + '/backup-settings')
|
|
]);
|
|
|
|
const backupsData = await backupsRes.json();
|
|
const settingsData = await settingsRes.json();
|
|
const backups = backupsData.backups || [];
|
|
const settings = settingsData.settings || {};
|
|
|
|
document.getElementById('backupTotalCount').textContent = backups.length;
|
|
document.getElementById('backupAutoCount').textContent = backups.filter(b => b.backup_type === 'auto').length;
|
|
|
|
if (backups.length > 0) {
|
|
const lastBackup = new Date(backups[0].created_at);
|
|
document.getElementById('backupLastTime').textContent = lastBackup.toLocaleDateString();
|
|
} else {
|
|
document.getElementById('backupLastTime').textContent = 'Never';
|
|
}
|
|
|
|
document.getElementById('backupAutoEnabled').value = settings.auto_enabled ? 'true' : 'false';
|
|
document.getElementById('backupInterval').value = settings.interval_hours || 24;
|
|
document.getElementById('backupMaxKeep').value = settings.max_backups || 7;
|
|
|
|
const tbody = document.getElementById('backupsList');
|
|
if (backups.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="7" class="empty-state">No backups yet. Create your first backup above.</td></tr>';
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = backups.map(b => {
|
|
const date = new Date(b.created_at).toLocaleString();
|
|
const typeIcon = b.backup_type === 'auto' ? '🔄 Auto' : '📁 Manual';
|
|
const size = formatBytes(b.size_bytes);
|
|
return '<tr>' +
|
|
'<td><strong>' + escapeHtml(b.name) + '</strong><br><small style="color:var(--muted)">' + escapeHtml(b.description || '') + '</small></td>' +
|
|
'<td>' + typeIcon + '</td>' +
|
|
'<td>' + b.roles_count + '</td>' +
|
|
'<td>' + b.channels_count + '</td>' +
|
|
'<td>' + size + '</td>' +
|
|
'<td>' + date + '</td>' +
|
|
'<td><button class="btn btn-secondary" style="padding:0.4rem 0.6rem;margin-right:0.25rem" onclick="viewBackup(\'' + b.id + '\')">View</button>' +
|
|
'<button class="btn btn-secondary" style="padding:0.4rem 0.6rem;background:var(--danger)" onclick="deleteBackup(\'' + b.id + '\')">Delete</button></td>' +
|
|
'</tr>';
|
|
}).join('');
|
|
} catch (e) {
|
|
console.error('Failed to load backups:', e);
|
|
document.getElementById('backupsList').innerHTML = '<tr><td colspan="7" class="empty-state">Failed to load backups</td></tr>';
|
|
}
|
|
}
|
|
|
|
function formatBytes(bytes) {
|
|
if (!bytes || bytes === 0) return '0 B';
|
|
const k = 1024;
|
|
const sizes = ['B', 'KB', 'MB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
}
|
|
|
|
async function createBackup() {
|
|
if (!currentGuild) return;
|
|
|
|
const name = document.getElementById('backupName').value || 'Dashboard Backup';
|
|
const description = document.getElementById('backupDescription').value || '';
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/backups', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name, description })
|
|
});
|
|
|
|
if (res.ok) {
|
|
document.getElementById('backupName').value = '';
|
|
document.getElementById('backupDescription').value = '';
|
|
await loadBackups();
|
|
alert('Backup created successfully!');
|
|
} else {
|
|
const err = await res.json();
|
|
alert(err.error || 'Failed to create backup');
|
|
}
|
|
} catch (e) {
|
|
alert('Failed to create backup');
|
|
}
|
|
}
|
|
|
|
async function saveBackupSettings() {
|
|
if (!currentGuild) return;
|
|
|
|
const auto_enabled = document.getElementById('backupAutoEnabled').value === 'true';
|
|
const interval_hours = parseInt(document.getElementById('backupInterval').value) || 24;
|
|
const max_backups = parseInt(document.getElementById('backupMaxKeep').value) || 7;
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/backup-settings', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ auto_enabled, interval_hours, max_backups })
|
|
});
|
|
|
|
if (res.ok) {
|
|
alert('Backup settings saved!');
|
|
} else {
|
|
alert('Failed to save settings');
|
|
}
|
|
} catch (e) {
|
|
alert('Failed to save settings');
|
|
}
|
|
}
|
|
|
|
async function viewBackup(backupId) {
|
|
if (!currentGuild) return;
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/backups/' + backupId);
|
|
const data = await res.json();
|
|
|
|
if (!data.backup) {
|
|
alert('Backup not found');
|
|
return;
|
|
}
|
|
|
|
const b = data.backup;
|
|
const bd = b.data;
|
|
|
|
document.getElementById('backupModalTitle').textContent = 'Backup: ' + b.name;
|
|
document.getElementById('backupModalBody').innerHTML =
|
|
'<div style="margin-bottom:1rem"><strong>Created:</strong> ' + new Date(b.created_at).toLocaleString() + '</div>' +
|
|
'<div style="margin-bottom:1rem"><strong>Type:</strong> ' + (b.backup_type === 'auto' ? 'Automatic' : 'Manual') + '</div>' +
|
|
'<div style="margin-bottom:1rem"><strong>Description:</strong> ' + escapeHtml(b.description || 'None') + '</div>' +
|
|
'<h4 style="margin:1rem 0 0.5rem">Roles (' + (bd.roles?.length || 0) + ')</h4>' +
|
|
'<div style="max-height:150px;overflow-y:auto;background:var(--secondary);padding:0.75rem;border-radius:8px;font-size:0.85rem">' +
|
|
(bd.roles?.map(r => '<span style="display:inline-block;margin:0.2rem;padding:0.2rem 0.5rem;background:var(--card);border-radius:4px;color:#' + (r.color || 'ffffff').toString(16).padStart(6,'0') + '">' + escapeHtml(r.name) + '</span>').join('') || 'No roles') +
|
|
'</div>' +
|
|
'<h4 style="margin:1rem 0 0.5rem">Channels (' + (bd.channels?.length || 0) + ')</h4>' +
|
|
'<div style="max-height:150px;overflow-y:auto;background:var(--secondary);padding:0.75rem;border-radius:8px;font-size:0.85rem">' +
|
|
(bd.channels?.map(c => '<span style="display:inline-block;margin:0.2rem;padding:0.2rem 0.5rem;background:var(--card);border-radius:4px">#' + escapeHtml(c.name) + '</span>').join('') || 'No channels') +
|
|
'</div>';
|
|
|
|
document.getElementById('backupModal').classList.remove('hidden');
|
|
} catch (e) {
|
|
alert('Failed to load backup details');
|
|
}
|
|
}
|
|
|
|
function closeBackupModal() {
|
|
document.getElementById('backupModal').classList.add('hidden');
|
|
}
|
|
|
|
async function deleteBackup(backupId) {
|
|
if (!confirm('Are you sure you want to delete this backup? This cannot be undone.')) return;
|
|
if (!currentGuild) return;
|
|
|
|
try {
|
|
const res = await fetch('/api/guild/' + currentGuild + '/backups/' + backupId, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
if (res.ok) {
|
|
await loadBackups();
|
|
} else {
|
|
alert('Failed to delete backup');
|
|
}
|
|
} catch (e) {
|
|
alert('Failed to delete backup');
|
|
}
|
|
}
|
|
|
|
function toggleMobileSidebar() {
|
|
const sidebar = document.getElementById('sidebar');
|
|
const overlay = document.getElementById('sidebarOverlay');
|
|
sidebar.classList.toggle('open');
|
|
overlay.classList.toggle('open');
|
|
document.body.style.overflow = sidebar.classList.contains('open') ? 'hidden' : '';
|
|
}
|
|
|
|
function closeMobileSidebar() {
|
|
const sidebar = document.getElementById('sidebar');
|
|
const overlay = document.getElementById('sidebarOverlay');
|
|
sidebar.classList.remove('open');
|
|
overlay.classList.remove('open');
|
|
document.body.style.overflow = '';
|
|
}
|
|
|
|
document.querySelectorAll('.nav-item').forEach(item => {
|
|
item.addEventListener('click', () => {
|
|
if (window.innerWidth <= 768) {
|
|
closeMobileSidebar();
|
|
}
|
|
});
|
|
});
|
|
|
|
init();
|
|
</script>
|
|
</body>
|
|
</html>
|